mirror of https://github.com/fail2ban/fail2ban
Merge branch '0.9'
commit
9bee8b3257
|
@ -6,3 +6,5 @@ htmlcov
|
|||
.coverage
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
__pycache__
|
||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -4,14 +4,18 @@ language: python
|
|||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "pypy"
|
||||
before_install:
|
||||
- sudo apt-get update -qq
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then sudo apt-get update -qq; fi
|
||||
install:
|
||||
- pip install pyinotify
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then sudo apt-get install -qq python-gamin; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then sudo apt-get install -qq python-gamin; cp /usr/share/pyshared/gamin.py /usr/lib/pyshared/python2.7/_gamin.so $VIRTUAL_ENV/lib/python2.7/site-packages/; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cd ..; pip install -q coveralls; cd -; fi
|
||||
script:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then export PYTHONPATH="$PYTHONPATH:/usr/share/pyshared:/usr/lib/pyshared/python2.7"; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coverage run --rcfile=.travis_coveragerc fail2ban-testcases; else python ./fail2ban-testcases; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coverage run --rcfile=.travis_coveragerc setup.py test; else python setup.py test; fi
|
||||
after_success:
|
||||
# Coverage config file must be .coveragerc for coveralls
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then cp -v .travis_coveragerc .coveragerc; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then coveralls; fi
|
||||
|
|
|
@ -4,3 +4,4 @@ branch = True
|
|||
omit =
|
||||
/usr/*
|
||||
/home/travis/virtualenv/*
|
||||
fail2ban/server/filtersystemd.py
|
||||
|
|
115
ChangeLog
115
ChangeLog
|
@ -4,9 +4,103 @@
|
|||
|_| \__,_|_|_/___|_.__/\__,_|_||_|
|
||||
|
||||
================================================================================
|
||||
Fail2Ban (version 0.8.12.dev) 2014/01/22
|
||||
Fail2Ban (version 0.9.0) 2014/03/14
|
||||
================================================================================
|
||||
|
||||
ver. 0.9.0 (2014/03/14 - beta
|
||||
----------
|
||||
|
||||
Carries all fixes, features and enhancements from 0.8.13 (unreleased) with
|
||||
major changes.
|
||||
|
||||
The minimum supported python version is now 2.6. If you have python-2.4 or 2.5
|
||||
you can use the 0.8.12 version of fail2ban.
|
||||
|
||||
Please take note of release notes:
|
||||
https://github.com/fail2ban/fail2ban/releases/tag/0.9.0
|
||||
|
||||
Please test your configuration before relying on it.
|
||||
|
||||
Nearly all development is thanks to Steven Hiscocks (THANKS!), merging,
|
||||
testcases and timezone support from Daniel Black, and code-review and minor
|
||||
additions from Yaroslav Halchenko.
|
||||
|
||||
- Refactoring (IMPORTANT -- Please review your setup and configuration):
|
||||
* [..bddbf1e] jail.conf was heavily refactored and now is similar
|
||||
to how it looked on Debian systems:
|
||||
- default action could be configured once for all jails
|
||||
- jails definitions only provide customizations (port, logpath)
|
||||
- no need to specify 'filter' if name matches jail name
|
||||
* [..5aef036] Core functionality moved into fail2ban/ module.
|
||||
Closes gh-26
|
||||
- tests included in module to aid testing and debugging
|
||||
* Added fail2ban persistent database
|
||||
- default location at /var/lib/fail2ban/fail2ban.sqlite3
|
||||
- allows active bans to be reinstated on restart
|
||||
- log files read from last position after restart
|
||||
* Added systemd journal backend
|
||||
- Dependency on python-systemd
|
||||
- New "journalmatch" option added to filter configs files
|
||||
- New "systemd-journal" option added to fail2ban-regex
|
||||
* Added python3 support
|
||||
* Support %z (Timezone offset) and %f (sub-seconds) support for
|
||||
datedetector. Enhanced existing date/time have been updated patterns to
|
||||
support these. ISO8601 now defaults to localtime unless specified otherwise.
|
||||
Some filters have been change as required to capture these elements in the
|
||||
right timezone correctly.
|
||||
* Log levels are now set by Syslog style strings e.g. DEBUG, ERROR.
|
||||
- Log level INFO is now more verbose
|
||||
* Optionally can read log files starting from "head" or "tail".
|
||||
- See "logpath" option in jail.conf(5) man page.
|
||||
* Can now set log encoding for files per jail.
|
||||
- Default uses systemd locale.
|
||||
|
||||
- New features:
|
||||
* [..c7ae460] Multiline failregex. Close gh-54
|
||||
* [8af32ed] Guacamole filter and support for Apache Tomcat date
|
||||
format
|
||||
* [..b6059f4] 'timeout' option for actions Close gh-60 and Debian bug
|
||||
#410077. Also it would now capture and include stdout and stderr
|
||||
into logging messages in case of error or at DEBUG loglevel.
|
||||
* Added action xarf-login-attack to report formatted attack messages
|
||||
according to the XARF standard (v0.2). Close gh-105
|
||||
* Support PyPy
|
||||
* Add filter for apache-botsearch
|
||||
* Add filter for kerio. Thanks Tony Lawrence for blog of regexs and
|
||||
providing samples. Close gh-120
|
||||
* Filter for stunnel
|
||||
* Filter for Counter Strike 1.6. Thanks to onorua for logs.
|
||||
Close gh-347
|
||||
* Filter for squirrelmail. Close gh-261
|
||||
* Filter for tine20. Close gh-583
|
||||
* Custom date formats (strptime) can now be set in filters and jail.conf
|
||||
* Python based actions can now be created.
|
||||
- SMTP action for sending emails on jail start, stop and ban.
|
||||
* Added action to use badips.com reporting and blacklist
|
||||
- Requires Python 2.7+
|
||||
|
||||
- Enhancements
|
||||
* Fail2ban-regex - don't accumulate lines if not printing them.
|
||||
add options to suppress output of missed/ignored lines. Close gh-644
|
||||
* Asterisk now supports syslog format
|
||||
* Jail names increased to 26 characters and iptables prefix reduced
|
||||
from fail2ban- to f2b- as suggested by buanzo in gh-462.
|
||||
* Multiline filter for sendmail-spam. Close gh-418
|
||||
* Multiline regex for Disconnecting: Too many authentication failures for
|
||||
root [preauth]\nConnection closed by 6X.XXX.XXX.XXX [preauth]
|
||||
* Multiline regex for Disconnecting: Connection from 61.XX.XX.XX port
|
||||
51353\nToo many authentication failures for root [preauth]. Thanks
|
||||
Helmut Grohne. Close gh-457
|
||||
* Replacing use of deprecated API (.warning, .assertEqual, etc)
|
||||
* [..a648cc2] Filters can have options now too which are substituted into
|
||||
failregex / ignoreregex
|
||||
* [..e019ab7] Multiple instances of the same action are allowed in the
|
||||
same jail -- use actname option to disambiguate.
|
||||
* Add honeypot email address to exim-spam filter as argument
|
||||
* Properties and methods of actions accessible from fail2ban-client
|
||||
- Use of properties replaces command actions "cinfo" interface
|
||||
|
||||
|
||||
ver. 0.8.13 (2014/XX/XXX) - maintenance-only-from-now-on
|
||||
-----------
|
||||
|
||||
|
@ -27,7 +121,7 @@ ver. 0.8.13 (2014/XX/XXX) - maintenance-only-from-now-on
|
|||
Thanks Noel Butler.
|
||||
|
||||
ver. 0.8.12 (2014/01/22) - things-can-only-get-better
|
||||
-----------
|
||||
----------
|
||||
|
||||
- IMPORTANT incompatible changes:
|
||||
- Rename firewall-cmd-direct-new to firewallcmd-new to fit within jail name
|
||||
|
@ -39,7 +133,7 @@ ver. 0.8.12 (2014/01/22) - things-can-only-get-better
|
|||
- 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. Thanksg Ioan Indreias
|
||||
- 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
|
||||
- add 'flushlogs' command to allow logrotation without clobbering logtarget
|
||||
|
@ -52,7 +146,7 @@ ver. 0.8.12 (2014/01/22) - things-can-only-get-better
|
|||
- Asynchat changed to use push method which verifys whether all data was
|
||||
send. This ensures that all data is sent before closing the connection.
|
||||
- Removed unnecessary reference to as yet undeclared $jail_name when checking
|
||||
a specific jail.
|
||||
a specific jail in nagios script.
|
||||
- Filter dovecot reordered session and TLS items in regex with wider scope
|
||||
for session characters. Thanks Ivo Truxa. Closes gh-586
|
||||
- A single bad failregex or command syntax in configuration files won't stop
|
||||
|
@ -68,8 +162,11 @@ ver. 0.8.12 (2014/01/22) - things-can-only-get-better
|
|||
Thanks dani. Closes gh-503
|
||||
- exim-spam filter to match spamassassin log entry for option SAdevnull.
|
||||
Thanks Ivo Truxa. Closes gh-533
|
||||
- filter.d/nsd.conf -- also amended Unix date template to match nsd format
|
||||
- Added to sshd filter expression for "Received disconnect from <HOST>: 3:
|
||||
...: Auth fail". Thanks Marcel Dopita. Closes gh-289
|
||||
- loglines now also report "[PID]" after the name portion
|
||||
- Added filter.d/ejabberd-auth
|
||||
- Improved ACL-handling for Asterisk
|
||||
- loglines now also report "[PID]" after the name portion
|
||||
- Added improper command pipelining to postfix filter.
|
||||
|
@ -79,10 +176,12 @@ ver. 0.8.12 (2014/01/22) - things-can-only-get-better
|
|||
- filter.d/solid-pop3d -- added thanks to Jacques Lav!gnotte on mailinglist.
|
||||
- Add filter for apache-modsecurity.
|
||||
- filter.d/nsd.conf -- also amended Unix date template to match nsd format
|
||||
- Added filter.d/openwebmail filter thanks Ivo Truxa. Closes gh-543
|
||||
- Added filter.d/horde.
|
||||
- Added openwebmail filter thanks Ivo Truxa. Closes gh-543
|
||||
- Added filter for freeswitch. Thanks Jim and editors and authors of
|
||||
http://wiki.freeswitch.org/wiki/Fail2ban.
|
||||
http://wiki.freeswitch.org/wiki/Fail2ban
|
||||
- Added groupoffice filter thanks to logs from Merijn Schering.
|
||||
Closes gh-566
|
||||
- Added filter for horde
|
||||
- Added filter for squid. Thanks Roman Gelfand.
|
||||
- Added filter for ejabberd-auth.
|
||||
- Added filter.d/openwebmail filter thanks Ivo Truxa. Closes gh-543
|
||||
|
@ -95,7 +194,7 @@ ver. 0.8.12 (2014/01/22) - things-can-only-get-better
|
|||
|
||||
|
||||
ver. 0.8.11 (2013/11/13) - loves-unittests-and-tight-DoS-free-filter-regexes
|
||||
-----------
|
||||
----------
|
||||
|
||||
In light of CVE-2013-2178 that triggered our last release we have put
|
||||
a significant effort into tightening all of the regexs of our filters
|
||||
|
|
164
DEVELOP
164
DEVELOP
|
@ -39,9 +39,9 @@ If you are developing filters see the FILTERS file for documentation.
|
|||
Code Testing
|
||||
============
|
||||
|
||||
Existing tests can be run by executing `fail2ban-testcases`. This has options
|
||||
like --log-level that will probably be useful. `fail2ban-testcases --help` for
|
||||
full options.
|
||||
Existing tests can be run by executing `bin/fail2ban-testcases`. It has
|
||||
options like --log-level that will probably be useful. Run
|
||||
`bin/fail2ban-testcases --help` for the full list of options.
|
||||
|
||||
Test cases should cover all usual cases, all exception cases and all inside
|
||||
/ outside boundary conditions.
|
||||
|
@ -54,7 +54,7 @@ Install the package python-coverage to visualise your test coverage. Run the
|
|||
following (note: on Debian-based systems, the script is called
|
||||
`python-coverage`):
|
||||
|
||||
coverage run fail2ban-testcases
|
||||
coverage run bin/fail2ban-testcases
|
||||
coverage html
|
||||
|
||||
Then look at htmlcov/index.html and see how much coverage your test cases
|
||||
|
@ -268,159 +268,3 @@ action.py
|
|||
|
||||
Takes care about executing start/check/ban/unban/stop commands
|
||||
|
||||
|
||||
Releasing
|
||||
=========
|
||||
|
||||
# Check distribution patches and see if they can be included
|
||||
|
||||
* https://apps.fedoraproject.org/packages/fail2ban/sources
|
||||
* http://sources.gentoo.org/cgi-bin/viewvc.cgi/gentoo-x86/net-analyzer/fail2ban/
|
||||
* http://svnweb.freebsd.org/ports/head/security/py-fail2ban/
|
||||
* https://build.opensuse.org/package/show?package=fail2ban&project=openSUSE%3AFactory
|
||||
* http://sophie.zarb.org/sources/fail2ban (Mageia)
|
||||
* https://trac.macports.org/browser/trunk/dports/security/fail2ban
|
||||
|
||||
# Check distribution outstanding bugs
|
||||
|
||||
* 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
|
||||
|
||||
./fail2ban-testcases-all
|
||||
|
||||
# Ensure the version is correct
|
||||
|
||||
in:
|
||||
* ./common/version.py
|
||||
* top of ChangeLog
|
||||
* README.md
|
||||
|
||||
# Ensure the MANIFEST is complete
|
||||
|
||||
Run:
|
||||
|
||||
python setup.py sdist
|
||||
|
||||
Look for errors like:
|
||||
'testcases/files/logs/mysqld.log' not a regular file -- skipping
|
||||
|
||||
Which indicates that testcases/files/logs/mysqld.log has been moved or is a directory
|
||||
|
||||
tar -C /tmp -jxf dist/fail2ban-0.8.12.tar.bz2
|
||||
|
||||
# clean up current direcory
|
||||
|
||||
diff -rul --exclude \*.pyc . /tmp/fail2ban-0.8.12/
|
||||
|
||||
# Only differences should be files that you don't want distributed.
|
||||
|
||||
# Ensure the tests work from the tarball
|
||||
|
||||
cd /tmp/fail2ban-0.8.12/ && ./fail2ban-testcases-all
|
||||
|
||||
# Add/finalize the corresponding entry in the ChangeLog
|
||||
|
||||
To generate a list of committers use e.g.
|
||||
|
||||
git shortlog -sn 0.8.11.. | sed -e 's,^[ 0-9\t]*,,g' | tr '\n' '\|' | sed -e 's:|:, :g'
|
||||
|
||||
Ensure the top of the ChangeLog has the right version and current date.
|
||||
|
||||
Ensure the top entry of the ChangeLog has the right version and current date.
|
||||
|
||||
# Update man pages
|
||||
|
||||
(cd man ; ./generate-man )
|
||||
git commit -m 'DOC/ENH: update man pages for release' man/*
|
||||
|
||||
# Prepare source and rpm binary distributions
|
||||
|
||||
python setup.py sdist
|
||||
python setup.py bdist_rpm
|
||||
python setup.py upload
|
||||
|
||||
# Provide a release sample to distributors
|
||||
|
||||
* Arch Linux:
|
||||
https://www.archlinux.org/packages/community/any/fail2ban/
|
||||
* Debian: Yaroslav Halchenko <debian@onerussian.com>
|
||||
http://packages.qa.debian.org/f/fail2ban.html
|
||||
* FreeBSD: Christoph Theis theis@gmx.at>, Nick Hilliard <nick@foobar.org>
|
||||
http://svnweb.freebsd.org/ports/head/security/py-fail2ban/Makefile?view=markup
|
||||
http://www.freebsd.org/cgi/query-pr-summary.cgi?text=fail2ban
|
||||
* Fedora: Axel Thimm <Axel.Thimm@atrpms.net>
|
||||
https://apps.fedoraproject.org/packages/fail2ban
|
||||
http://pkgs.fedoraproject.org/cgit/fail2ban.git
|
||||
https://admin.fedoraproject.org/pkgdb/acls/bugs/fail2ban
|
||||
* Gentoo: netmon@gentoo.org
|
||||
http://sources.gentoo.org/cgi-bin/viewvc.cgi/gentoo-x86/net-analyzer/fail2ban/metadata.xml?view=markup
|
||||
https://bugs.gentoo.org/buglist.cgi?quicksearch=fail2ban
|
||||
* openSUSE: Stephan Kulow <coolo@suse.com>
|
||||
https://build.opensuse.org/package/show/openSUSE:Factory/fail2ban
|
||||
* Mac Ports: @Malbrouck on github (gh-49)
|
||||
https://trac.macports.org/browser/trunk/dports/security/fail2ban/Portfile
|
||||
* Mageia:
|
||||
https://bugs.mageia.org/buglist.cgi?quicksearch=fail2ban
|
||||
An potentially to the fail2ban-users directory.
|
||||
|
||||
# Wait for feedback from distributors
|
||||
|
||||
# Prepare a release notice https://github.com/fail2ban/fail2ban/releases/new
|
||||
|
||||
Upload the source/binaries from the dist directory and tag the release using the URL
|
||||
|
||||
# Upload source/binaries to sourceforge http://sourceforge.net/projects/fail2ban/
|
||||
|
||||
# Run the following and update the wiki with output:
|
||||
python -c 'import common.protocol; common.protocol.printWiki()'
|
||||
|
||||
page: http://www.fail2ban.org/wiki/index.php/Commands
|
||||
|
||||
* Update:
|
||||
http://www.fail2ban.org/wiki/index.php?title=Template:Fail2ban_Versions&action=edit
|
||||
|
||||
http://www.fail2ban.org/wiki/index.php?title=Template:Fail2ban_News&action=edit
|
||||
move old bits to:
|
||||
http://www.fail2ban.org/wiki/index.php?title=Template:Fail2ban_OldNews&action=edit
|
||||
|
||||
http://www.fail2ban.org/wiki/index.php?title=Template:Fail2ban_Versions&action=edit
|
||||
http://www.fail2ban.org/wiki/index.php/ChangeLog
|
||||
http://www.fail2ban.org/wiki/index.php/Requirements (Check requirement)
|
||||
http://www.fail2ban.org/wiki/index.php/Features
|
||||
|
||||
* See if any filters are upgraded:
|
||||
http://www.fail2ban.org/wiki/index.php/Special:AllPages
|
||||
|
||||
# Email users and development list of release
|
||||
|
||||
# notify distributors
|
||||
|
||||
Post Release
|
||||
============
|
||||
|
||||
Add the following to the top of the ChangeLog
|
||||
|
||||
ver. 0.8.13 (2014/XX/XXX) - wanna-be-released
|
||||
-----------
|
||||
|
||||
- Fixes:
|
||||
|
||||
- New Features:
|
||||
|
||||
- Enhancements:
|
||||
|
||||
Alter the git shortlog command in the previous section to refer to the just
|
||||
released version.
|
||||
|
||||
and adjust common/version.py to carry .dev suffix to signal
|
||||
a version under development.
|
||||
|
|
324
MANIFEST
324
MANIFEST
|
@ -6,167 +6,202 @@ THANKS
|
|||
COPYING
|
||||
DEVELOP
|
||||
FILTERS
|
||||
fail2ban-client
|
||||
fail2ban-server
|
||||
fail2ban-testcases
|
||||
fail2ban-regex
|
||||
fail2ban-2to3
|
||||
fail2ban-testcases-all
|
||||
fail2ban-testcases-all-python3
|
||||
bin/fail2ban-client
|
||||
bin/fail2ban-server
|
||||
bin/fail2ban-testcases
|
||||
bin/fail2ban-regex
|
||||
doc/run-rootless.txt
|
||||
fail2ban/client/configreader.py
|
||||
fail2ban/client/configparserinc.py
|
||||
fail2ban/client/jailreader.py
|
||||
fail2ban/client/fail2banreader.py
|
||||
fail2ban/client/jailsreader.py
|
||||
fail2ban/client/beautifier.py
|
||||
fail2ban/client/filterreader.py
|
||||
fail2ban/client/actionreader.py
|
||||
fail2ban/client/__init__.py
|
||||
fail2ban/client/configurator.py
|
||||
fail2ban/client/csocket.py
|
||||
fail2ban/server/asyncserver.py
|
||||
fail2ban/server/database.py
|
||||
fail2ban/server/filter.py
|
||||
fail2ban/server/filterpyinotify.py
|
||||
fail2ban/server/filtergamin.py
|
||||
fail2ban/server/filterpoll.py
|
||||
fail2ban/server/filtersystemd.py
|
||||
fail2ban/server/iso8601.py
|
||||
fail2ban/server/server.py
|
||||
fail2ban/server/actions.py
|
||||
fail2ban/server/faildata.py
|
||||
fail2ban/server/failmanager.py
|
||||
fail2ban/server/datedetector.py
|
||||
fail2ban/server/jailthread.py
|
||||
fail2ban/server/transmitter.py
|
||||
fail2ban/server/action.py
|
||||
fail2ban/server/ticket.py
|
||||
fail2ban/server/jail.py
|
||||
fail2ban/server/jails.py
|
||||
fail2ban/server/__init__.py
|
||||
fail2ban/server/banmanager.py
|
||||
fail2ban/server/datetemplate.py
|
||||
fail2ban/server/mytime.py
|
||||
fail2ban/server/failregex.py
|
||||
fail2ban/server/database.py
|
||||
fail2ban/tests/banmanagertestcase.py
|
||||
fail2ban/tests/failmanagertestcase.py
|
||||
fail2ban/tests/clientreadertestcase.py
|
||||
fail2ban/tests/filtertestcase.py
|
||||
fail2ban/tests/__init__.py
|
||||
fail2ban/tests/dummyjail.py
|
||||
fail2ban/tests/samplestestcase.py
|
||||
fail2ban/tests/datedetectortestcase.py
|
||||
fail2ban/tests/actiontestcase.py
|
||||
fail2ban/tests/servertestcase.py
|
||||
fail2ban/tests/sockettestcase.py
|
||||
fail2ban/tests/utils.py
|
||||
fail2ban/tests/misctestcase.py
|
||||
fail2ban/tests/databasetestcase.py
|
||||
fail2ban/tests/config/jail.conf
|
||||
fail2ban/tests/config/fail2ban.conf
|
||||
fail2ban/tests/config/paths-common.conf
|
||||
fail2ban/tests/config/paths-freebsd.conf
|
||||
fail2ban/tests/config/paths-osx.conf
|
||||
fail2ban/tests/config/paths-debian.conf
|
||||
fail2ban/tests/config/filter.d/simple.conf
|
||||
fail2ban/tests/config/action.d/brokenaction.conf
|
||||
fail2ban/tests/files/config/apache-auth/digest/.htaccess
|
||||
fail2ban/tests/files/config/apache-auth/digest/.htpasswd
|
||||
fail2ban/tests/files/config/apache-auth/digest_time/.htaccess
|
||||
fail2ban/tests/files/config/apache-auth/digest_time/.htpasswd
|
||||
fail2ban/tests/files/config/apache-auth/basic/authz_owner/.htaccess
|
||||
fail2ban/tests/files/config/apache-auth/basic/authz_owner/cant_get_me.html
|
||||
fail2ban/tests/files/config/apache-auth/basic/authz_owner/.htpasswd
|
||||
fail2ban/tests/files/config/apache-auth/basic/file/.htaccess
|
||||
fail2ban/tests/files/config/apache-auth/basic/file/.htpasswd
|
||||
fail2ban/tests/files/config/apache-auth/digest.py
|
||||
fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htaccess
|
||||
fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htpasswd
|
||||
fail2ban/tests/files/config/apache-auth/digest_anon/.htaccess
|
||||
fail2ban/tests/files/config/apache-auth/digest_anon/.htpasswd
|
||||
fail2ban/tests/files/config/apache-auth/README
|
||||
fail2ban/tests/files/config/apache-auth/noentry/.htaccess
|
||||
fail2ban/tests/files/database_v1.db
|
||||
fail2ban/tests/files/ignorecommand.py
|
||||
fail2ban/tests/files/filter.d/substition.conf
|
||||
fail2ban/tests/files/filter.d/testcase-common.conf
|
||||
fail2ban/tests/files/filter.d/testcase01.conf
|
||||
fail2ban/tests/files/testcase01.log
|
||||
fail2ban/tests/files/testcase02.log
|
||||
fail2ban/tests/files/testcase03.log
|
||||
fail2ban/tests/files/testcase04.log
|
||||
fail2ban/tests/files/testcase-usedns.log
|
||||
fail2ban/tests/files/testcase-journal.log
|
||||
fail2ban/tests/files/testcase-multiline.log
|
||||
fail2ban/tests/files/logs/bsd/syslog-plain.txt
|
||||
fail2ban/tests/files/logs/bsd/syslog-v.txt
|
||||
fail2ban/tests/files/logs/bsd/syslog-vv.txt
|
||||
fail2ban/tests/files/logs/3proxy
|
||||
fail2ban/tests/files/logs/apache-auth
|
||||
fail2ban/tests/files/logs/apache-badbots
|
||||
fail2ban/tests/files/logs/apache-botscripts
|
||||
fail2ban/tests/files/logs/apache-modsecurity
|
||||
fail2ban/tests/files/logs/apache-nohome
|
||||
fail2ban/tests/files/logs/apache-noscript
|
||||
fail2ban/tests/files/logs/apache-overflows
|
||||
fail2ban/tests/files/logs/assp
|
||||
fail2ban/tests/files/logs/asterisk
|
||||
fail2ban/tests/files/logs/counter-strike
|
||||
fail2ban/tests/files/logs/courier-auth
|
||||
fail2ban/tests/files/logs/courier-smtp
|
||||
fail2ban/tests/files/logs/cyrus-imap
|
||||
fail2ban/tests/files/logs/dovecot
|
||||
fail2ban/tests/files/logs/dropbear
|
||||
fail2ban/tests/files/logs/ejabberd-auth
|
||||
fail2ban/tests/files/logs/exim
|
||||
fail2ban/tests/files/logs/exim-spam
|
||||
fail2ban/tests/files/logs/freeswitch
|
||||
fail2ban/tests/files/logs/groupoffice
|
||||
fail2ban/tests/files/logs/gssftpd
|
||||
fail2ban/tests/files/logs/guacamole
|
||||
fail2ban/tests/files/logs/kerio
|
||||
fail2ban/tests/files/logs/lighttpd-auth
|
||||
fail2ban/tests/files/logs/mysqld-auth
|
||||
fail2ban/tests/files/logs/nsd
|
||||
fail2ban/tests/files/logs/perdition
|
||||
fail2ban/tests/files/logs/php-url-fopen
|
||||
fail2ban/tests/files/logs/postfix-sasl
|
||||
fail2ban/tests/files/logs/named-refused
|
||||
fail2ban/tests/files/logs/nginx-http-auth
|
||||
fail2ban/tests/files/logs/pam-generic
|
||||
fail2ban/tests/files/logs/postfix
|
||||
fail2ban/tests/files/logs/proftpd
|
||||
fail2ban/tests/files/logs/pure-ftpd
|
||||
fail2ban/tests/files/logs/qmail
|
||||
fail2ban/tests/files/logs/recidive
|
||||
fail2ban/tests/files/logs/roundcube-auth
|
||||
fail2ban/tests/files/logs/selinux-ssh
|
||||
fail2ban/tests/files/logs/sendmail-spam
|
||||
fail2ban/tests/files/logs/sieve
|
||||
fail2ban/tests/files/logs/squid
|
||||
fail2ban/tests/files/logs/stunnel
|
||||
fail2ban/tests/files/logs/suhosin
|
||||
fail2ban/tests/files/logs/sogo-auth
|
||||
fail2ban/tests/files/logs/solid-pop3d
|
||||
fail2ban/tests/files/logs/sshd
|
||||
fail2ban/tests/files/logs/sshd-ddos
|
||||
fail2ban/tests/files/logs/vsftpd
|
||||
fail2ban/tests/files/logs/webmin-auth
|
||||
fail2ban/tests/files/logs/wuftpd
|
||||
fail2ban/tests/files/logs/uwimap-auth
|
||||
fail2ban/tests/files/logs/xinetd-fail
|
||||
fail2ban/tests/config/jail.conf
|
||||
fail2ban/tests/config/fail2ban.conf
|
||||
fail2ban/tests/config/filter.d/simple.conf
|
||||
fail2ban/tests/config/action.d/brokenaction.conf
|
||||
setup.py
|
||||
setup.cfg
|
||||
fail2ban/__init__.py
|
||||
fail2ban/exceptions.py
|
||||
fail2ban/helpers.py
|
||||
fail2ban/version.py
|
||||
fail2ban/protocol.py
|
||||
setup.py
|
||||
setup.cfg
|
||||
kill-server
|
||||
client/configreader.py
|
||||
client/configparserinc.py
|
||||
client/jailreader.py
|
||||
client/fail2banreader.py
|
||||
client/jailsreader.py
|
||||
client/beautifier.py
|
||||
client/filterreader.py
|
||||
client/actionreader.py
|
||||
client/__init__.py
|
||||
client/configurator.py
|
||||
client/csocket.py
|
||||
server/asyncserver.py
|
||||
server/filter.py
|
||||
server/filterpyinotify.py
|
||||
server/filtergamin.py
|
||||
server/filterpoll.py
|
||||
server/iso8601.py
|
||||
server/server.py
|
||||
server/actions.py
|
||||
server/faildata.py
|
||||
server/failmanager.py
|
||||
server/datedetector.py
|
||||
server/jailthread.py
|
||||
server/transmitter.py
|
||||
server/action.py
|
||||
server/ticket.py
|
||||
server/jail.py
|
||||
server/jails.py
|
||||
server/__init__.py
|
||||
server/banmanager.py
|
||||
server/datetemplate.py
|
||||
server/mytime.py
|
||||
server/failregex.py
|
||||
testcases/actionstestcase.py
|
||||
testcases/dummyjail.py
|
||||
testcases/files/ignorecommand.py
|
||||
testcases/files/testcase-usedns.log
|
||||
testcases/files/logs/bsd/syslog-plain.txt
|
||||
testcases/files/logs/bsd/syslog-v.txt
|
||||
testcases/files/logs/bsd/syslog-vv.txt
|
||||
testcases/files/logs/apache-overflows
|
||||
testcases/files/logs/apache-modsecurity
|
||||
testcases/files/logs/assp
|
||||
testcases/files/logs/asterisk
|
||||
testcases/files/logs/dovecot
|
||||
testcases/files/logs/ejabberd-auth
|
||||
testcases/files/logs/exim
|
||||
testcases/files/logs/freeswitch
|
||||
testcases/files/logs/groupoffice
|
||||
testcases/files/logs/horde
|
||||
testcases/files/logs/suhosin
|
||||
testcases/files/logs/mysqld-auth
|
||||
testcases/files/logs/named-refused
|
||||
testcases/files/logs/nginx-http-auth
|
||||
testcases/files/logs/nsd
|
||||
testcases/files/logs/openwebmail
|
||||
testcases/files/logs/pam-generic
|
||||
testcases/files/logs/postfix
|
||||
testcases/files/logs/proftpd
|
||||
testcases/files/logs/pure-ftpd
|
||||
testcases/files/logs/roundcube-auth
|
||||
testcases/files/logs/postfix-sasl
|
||||
testcases/files/logs/sogo-auth
|
||||
testcases/files/logs/solid-pop3d
|
||||
testcases/files/logs/squid
|
||||
testcases/files/logs/sshd
|
||||
testcases/files/logs/sshd-ddos
|
||||
testcases/files/logs/vsftpd
|
||||
testcases/files/logs/webmin-auth
|
||||
testcases/files/logs/wuftpd
|
||||
testcases/files/logs/3proxy
|
||||
testcases/files/logs/apache-auth
|
||||
testcases/files/logs/apache-badbots
|
||||
testcases/files/logs/apache-nohome
|
||||
testcases/files/logs/apache-noscript
|
||||
testcases/files/logs/courierlogin
|
||||
testcases/files/logs/couriersmtp
|
||||
testcases/files/logs/cyrus-imap
|
||||
testcases/files/logs/dropbear
|
||||
testcases/files/logs/exim-spam
|
||||
testcases/files/logs/gssftpd
|
||||
testcases/files/logs/lighttpd-auth
|
||||
testcases/files/logs/mysqld-auth
|
||||
testcases/files/logs/perdition
|
||||
testcases/files/logs/php-url-fopen
|
||||
testcases/files/logs/qmail
|
||||
testcases/files/logs/recidive
|
||||
testcases/files/logs/sieve
|
||||
testcases/files/logs/selinux-ssh
|
||||
testcases/files/logs/sendmail-auth
|
||||
testcases/files/logs/sendmail-reject
|
||||
testcases/files/logs/suhosin
|
||||
testcases/files/logs/uwimap-auth
|
||||
testcases/files/logs/wuftpd
|
||||
testcases/files/logs/xinetd-fail
|
||||
testcases/files/config/apache-auth/digest/.htaccess
|
||||
testcases/files/config/apache-auth/digest/.htpasswd
|
||||
testcases/files/config/apache-auth/digest_time/.htaccess
|
||||
testcases/files/config/apache-auth/digest_time/.htpasswd
|
||||
testcases/files/config/apache-auth/basic/authz_owner/.htaccess
|
||||
testcases/files/config/apache-auth/basic/authz_owner/cant_get_me.html
|
||||
testcases/files/config/apache-auth/basic/authz_owner/.htpasswd
|
||||
testcases/files/config/apache-auth/basic/file/.htaccess
|
||||
testcases/files/config/apache-auth/basic/file/.htpasswd
|
||||
testcases/files/config/apache-auth/digest.py
|
||||
testcases/files/config/apache-auth/digest_wrongrelm/.htaccess
|
||||
testcases/files/config/apache-auth/digest_wrongrelm/.htpasswd
|
||||
testcases/files/config/apache-auth/digest_anon/.htaccess
|
||||
testcases/files/config/apache-auth/digest_anon/.htpasswd
|
||||
testcases/files/config/apache-auth/README
|
||||
testcases/files/config/apache-auth/noentry/.htaccess
|
||||
testcases/samplestestcase.py
|
||||
testcases/banmanagertestcase.py
|
||||
testcases/failmanagertestcase.py
|
||||
testcases/clientreadertestcase.py
|
||||
testcases/filtertestcase.py
|
||||
testcases/__init__.py
|
||||
testcases/datedetectortestcase.py
|
||||
testcases/actiontestcase.py
|
||||
testcases/servertestcase.py
|
||||
testcases/sockettestcase.py
|
||||
testcases/files/testcase01.log
|
||||
testcases/files/testcase02.log
|
||||
testcases/files/testcase03.log
|
||||
testcases/files/testcase04.log
|
||||
testcases/misctestcase.py
|
||||
testcases/utils.py
|
||||
common/__init__.py
|
||||
common/exceptions.py
|
||||
common/helpers.py
|
||||
common/version.py
|
||||
common/protocol.py
|
||||
config/jail.conf
|
||||
config/fail2ban.conf
|
||||
config/filter.d/common.conf
|
||||
config/filter.d/apache-auth.conf
|
||||
config/filter.d/apache-badbots.conf
|
||||
config/filter.d/apache-botsearch.conf
|
||||
config/filter.d/apache-modsecurity.conf
|
||||
config/filter.d/apache-nohome.conf
|
||||
config/filter.d/apache-noscript.conf
|
||||
config/filter.d/apache-overflows.conf
|
||||
config/filter.d/nginx-http-auth.conf
|
||||
config/filter.d/courierlogin.conf
|
||||
config/filter.d/couriersmtp.conf
|
||||
config/filter.d/counter-strike.conf
|
||||
config/filter.d/courier-auth.conf
|
||||
config/filter.d/courier-smtp.conf
|
||||
config/filter.d/cyrus-imap.conf
|
||||
config/filter.d/ejabberd-auth.conf
|
||||
config/filter.d/exim.conf
|
||||
config/filter.d/freeswitch.conf
|
||||
config/filter.d/gssftpd.conf
|
||||
config/filter.d/kerio.conf
|
||||
config/filter.d/horde.conf
|
||||
config/filter.d/suhosin.conf
|
||||
config/filter.d/named-refused.conf
|
||||
config/filter.d/nsd.conf
|
||||
config/filter.d/openwebmail.conf
|
||||
config/filter.d/pam-generic.conf
|
||||
config/filter.d/php-url-fopen.conf
|
||||
config/filter.d/postfix-sasl.conf
|
||||
config/filter.d/pam-generic.conf
|
||||
config/filter.d/php-url-fopen.conf
|
||||
config/filter.d/postfix-sasl.conf
|
||||
config/filter.d/postfix.conf
|
||||
config/filter.d/proftpd.conf
|
||||
config/filter.d/pure-ftpd.conf
|
||||
|
@ -181,6 +216,7 @@ config/filter.d/solid-pop3d.conf
|
|||
config/filter.d/squid.conf
|
||||
config/filter.d/sshd.conf
|
||||
config/filter.d/sshd-ddos.conf
|
||||
config/filter.d/stunnel.conf
|
||||
config/filter.d/vsftpd.conf
|
||||
config/filter.d/webmin-auth.conf
|
||||
config/filter.d/wuftpd.conf
|
||||
|
@ -200,9 +236,15 @@ config/filter.d/3proxy.conf
|
|||
config/filter.d/apache-common.conf
|
||||
config/filter.d/exim-common.conf
|
||||
config/filter.d/exim-spam.conf
|
||||
config/filter.d/freeswitch.conf
|
||||
config/filter.d/groupoffice.conf
|
||||
config/filter.d/perdition.conf
|
||||
config/filter.d/uwimap-auth.conf
|
||||
config/filter.d/courier-auth.conf
|
||||
config/filter.d/courier-smtp.conf
|
||||
config/filter.d/ejabberd-auth.conf
|
||||
config/filter.d/guacamole.conf
|
||||
config/filter.d/sendmail-spam.conf
|
||||
config/action.d/apf.conf
|
||||
config/action.d/blocklist_de.conf
|
||||
config/action.d/osx-afctl.conf
|
||||
|
@ -237,9 +279,11 @@ config/action.d/mynetwatchman.conf
|
|||
config/action.d/pf.conf
|
||||
config/action.d/sendmail.conf
|
||||
config/action.d/sendmail-buffered.conf
|
||||
config/action.d/sendmail-whois-ipmatches.conf
|
||||
config/action.d/sendmail-whois.conf
|
||||
config/action.d/sendmail-whois-lines.conf
|
||||
config/action.d/shorewall.conf
|
||||
config/action.d/xarf-login-attack.conf
|
||||
config/action.d/ufw.conf
|
||||
config/fail2ban.conf
|
||||
doc/run-rootless.txt
|
||||
|
@ -270,7 +314,3 @@ 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
|
||||
|
|
21
README.md
21
README.md
|
@ -2,7 +2,7 @@
|
|||
/ _|__ _(_) |_ ) |__ __ _ _ _
|
||||
| _/ _` | | |/ /| '_ \/ _` | ' \
|
||||
|_| \__,_|_|_/___|_.__/\__,_|_||_|
|
||||
v0.8.12 2014/01/22
|
||||
v0.9.0 2014/03/14
|
||||
|
||||
## Fail2Ban: ban hosts that cause multiple authentication errors
|
||||
|
||||
|
@ -11,6 +11,11 @@ password failures. It updates firewall rules to reject the IP address. These
|
|||
rules can be defined by the user. Fail2Ban can read multiple log files such as
|
||||
sshd or Apache web server ones.
|
||||
|
||||
Fail2Ban is able to reduce the rate of incorrect authentications attempts
|
||||
however it cannot eliminate the risk that weak authentication presents.
|
||||
Configure services to use only two factor or public/private authentication
|
||||
mechanisms if you really want to protect services.
|
||||
|
||||
This README is a quick introduction to Fail2ban. More documentation, FAQ, HOWTOs
|
||||
are available in fail2ban(1) manpage and on the website http://www.fail2ban.org
|
||||
|
||||
|
@ -21,21 +26,22 @@ Installation:
|
|||
this case, you should use it instead.**
|
||||
|
||||
Required:
|
||||
- [Python >= 2.4](http://www.python.org)
|
||||
- [Python2 >= 2.6 or Python >= 3.2](http://www.python.org) or [PyPy](http://pypy.org)
|
||||
|
||||
Optional:
|
||||
- [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify)
|
||||
- Linux >= 2.6.13
|
||||
- [gamin >= 0.0.21](http://www.gnome.org/~veillard/gamin)
|
||||
- [systemd >= 204](http://www.freedesktop.org/wiki/Software/systemd)
|
||||
|
||||
To install, just do:
|
||||
|
||||
tar xvfj fail2ban-0.8.12.tar.bz2
|
||||
cd fail2ban-0.8.12
|
||||
tar xvfj fail2ban-0.9.0.tar.bz2
|
||||
cd fail2ban-0.9.0
|
||||
python setup.py install
|
||||
|
||||
This will install Fail2Ban into /usr/share/fail2ban. The executable scripts are
|
||||
placed into /usr/bin, and configuration under /etc/fail2ban.
|
||||
This will install Fail2Ban into the python library directory. The executable
|
||||
scripts are placed into /usr/bin, and configuration under /etc/fail2ban.
|
||||
|
||||
Fail2Ban should be correctly installed now. Just type:
|
||||
|
||||
|
@ -50,8 +56,7 @@ Configuration:
|
|||
You can configure Fail2Ban using the files in /etc/fail2ban. It is possible to
|
||||
configure the server using commands sent to it by fail2ban-client. The
|
||||
available commands are described in the fail2ban-client(1) manpage. Also see
|
||||
fail2ban(1) manpage for further references and find even more documentation on
|
||||
the website: http://www.fail2ban.org
|
||||
fail2ban(1) and jail.conf(5) manpages for further references.
|
||||
|
||||
Code status:
|
||||
------------
|
||||
|
|
8
THANKS
8
THANKS
|
@ -40,9 +40,12 @@ Frédéric
|
|||
Georgiy Mernov
|
||||
Guilhem Lettron
|
||||
Guillaume Delvit
|
||||
Hank Leininger
|
||||
Hanno 'Rince' Wagner
|
||||
Helmut Grohne
|
||||
Iain Lea
|
||||
Ivo Truxa
|
||||
John Thoe
|
||||
Jacques Lav!gnotte
|
||||
Ioan Indreias
|
||||
Jonathan Kamens
|
||||
|
@ -55,6 +58,7 @@ Justin Shore
|
|||
Kévin Drapel
|
||||
kjohnsonecl
|
||||
kojiro
|
||||
Lars Kneschke
|
||||
Lee Clemens
|
||||
Manuel Arostegui Ramirez
|
||||
Marcel Dopita
|
||||
|
@ -68,7 +72,9 @@ mEDI
|
|||
Merijn Schering
|
||||
Michael C. Haller
|
||||
Michael Hanselmann
|
||||
Mika (mkl)
|
||||
Nick Munger
|
||||
onorua
|
||||
Noel Butler
|
||||
Patrick Börjesson
|
||||
Raphaël Marichez
|
||||
|
@ -84,8 +90,10 @@ silviogarbes
|
|||
Stefan Tatschner
|
||||
Stephen Gildea
|
||||
Steven Hiscocks
|
||||
TESTOVIK
|
||||
Tom Pike
|
||||
Tomas Pihl
|
||||
Tony Lawrence
|
||||
Tomasz Ciolek
|
||||
Tyler
|
||||
Vaclav Misek
|
||||
|
|
27
TODO
27
TODO
|
@ -13,20 +13,6 @@ Legend:
|
|||
# partially done
|
||||
* done
|
||||
|
||||
- more detailed explaination in DEVELOP for new developers (eg. howto build this HEX numbers in ChangeLog)
|
||||
|
||||
- Run tests though all filters/examples files - (see sshd example file) as unit
|
||||
test
|
||||
|
||||
* Removed relative imports
|
||||
|
||||
* Cleanup fail2ban-client and fail2ban-server. Move code to server/ and client/
|
||||
|
||||
- Add timeout to external commands (signal alarm, watchdog thread, etc)
|
||||
|
||||
- Uniformize filters and actions name. Use the software name (openssh, postfix,
|
||||
proftp) and possible qualifier (e.g. auth) after a '-'
|
||||
|
||||
- Added <USER> tag for failregex. Add features using this information. Maybe add
|
||||
more tags
|
||||
|
||||
|
@ -37,23 +23,10 @@ Legend:
|
|||
- Auto-enable function (search for log files), check modification date to see if
|
||||
service is still in use
|
||||
|
||||
- Improve parsing of the action parameters in jailreader.py
|
||||
|
||||
- Better handling of the protocol in transmitter.py
|
||||
|
||||
- Add gettext support (I18N)
|
||||
|
||||
- Multiline log reading
|
||||
|
||||
- Improve execution of action. Why does subprocess.call deadlock with
|
||||
multi-jails?
|
||||
|
||||
# see Feature Request Tracking System at SourceForge.net
|
||||
|
||||
# improve documentation and website for user
|
||||
|
||||
# better return values in function
|
||||
|
||||
# refactoring in server.py, actions.py, filter.py
|
||||
|
||||
* New backend: pyinotify
|
||||
|
|
|
@ -25,19 +25,11 @@ __license__ = "GPL"
|
|||
import sys, string, os, pickle, re, logging, signal
|
||||
import getopt, time, shlex, socket
|
||||
|
||||
# Inserts our own modules path first in the list
|
||||
# fix for bug #343821
|
||||
try:
|
||||
from common.version import version
|
||||
except ImportError, e:
|
||||
sys.path.insert(1, "/usr/share/fail2ban")
|
||||
from common.version import version
|
||||
|
||||
# Now we can import the rest of modules
|
||||
from common.protocol import printFormatted
|
||||
from client.csocket import CSocket
|
||||
from client.configurator import Configurator
|
||||
from client.beautifier import Beautifier
|
||||
from fail2ban.version import version
|
||||
from fail2ban.protocol import printFormatted
|
||||
from fail2ban.client.csocket import CSocket
|
||||
from fail2ban.client.configurator import Configurator
|
||||
from fail2ban.client.beautifier import Beautifier
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client")
|
||||
|
@ -110,7 +102,7 @@ class Fail2banClient:
|
|||
def __sigTERMhandler(self, signum, frame):
|
||||
# Print a new line because we probably come from wait
|
||||
print
|
||||
logSys.warn("Caught signal %d. Exiting" % signum)
|
||||
logSys.warning("Caught signal %d. Exiting" % signum)
|
||||
sys.exit(-1)
|
||||
|
||||
def __getCmdLineOptions(self, optList):
|
||||
|
@ -333,7 +325,7 @@ class Fail2banClient:
|
|||
if verbose <= 0:
|
||||
logSys.setLevel(logging.ERROR)
|
||||
elif verbose == 1:
|
||||
logSys.setLevel(logging.WARN)
|
||||
logSys.setLevel(logging.WARNING)
|
||||
elif verbose == 2:
|
||||
logSys.setLevel(logging.INFO)
|
||||
else:
|
|
@ -29,24 +29,23 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
|
|||
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2013 Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
import getopt, sys, time, logging, os, urllib
|
||||
|
||||
# Inserts our own modules path first in the list
|
||||
# fix for bug #343821
|
||||
try:
|
||||
from common.version import version
|
||||
except ImportError, e:
|
||||
sys.path.insert(1, "/usr/share/fail2ban")
|
||||
from common.version import version
|
||||
|
||||
import getopt, sys, time, logging, os, locale, shlex, urllib
|
||||
from optparse import OptionParser, Option
|
||||
|
||||
from client.configparserinc import SafeConfigParserWithIncludes
|
||||
from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError
|
||||
from server.filter import Filter
|
||||
from server.failregex import RegexException
|
||||
|
||||
from testcases.utils import FormatterWithTraceBack
|
||||
try:
|
||||
from systemd import journal
|
||||
from fail2ban.server.filtersystemd import FilterSystemd
|
||||
except ImportError:
|
||||
journal = None
|
||||
|
||||
from fail2ban.version import version
|
||||
from fail2ban.client.filterreader import FilterReader
|
||||
from fail2ban.server.filter import Filter
|
||||
from fail2ban.server.failregex import RegexException
|
||||
|
||||
from fail2ban.tests.utils import FormatterWithTraceBack
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban")
|
||||
|
||||
|
@ -72,6 +71,24 @@ def pprint_list(l, header=None):
|
|||
s = ''
|
||||
print s + "| " + "\n| ".join(l) + '\n`-'
|
||||
|
||||
def file_lines_gen(hdlr):
|
||||
for line in hdlr:
|
||||
try:
|
||||
line = line.decode(fail2banRegex.encoding, 'strict')
|
||||
except UnicodeDecodeError:
|
||||
if sys.version_info >= (3,): # Python 3 must be decoded
|
||||
line = line.decode(fail2banRegex.encoding, 'ignore')
|
||||
yield line
|
||||
|
||||
def journal_lines_gen(myjournal):
|
||||
while True:
|
||||
try:
|
||||
entry = myjournal.get_next()
|
||||
except OSError:
|
||||
continue
|
||||
if not entry:
|
||||
break
|
||||
yield FilterSystemd.formatJournalEntry(entry)
|
||||
|
||||
def get_opt_parser():
|
||||
# use module docstring for help output
|
||||
|
@ -81,6 +98,7 @@ def get_opt_parser():
|
|||
LOG:
|
||||
string a string representing a log line
|
||||
filename path to a log file (/var/log/auth.log)
|
||||
"systemd-journal" search systemd journal (systemd-python required)
|
||||
|
||||
REGEX:
|
||||
string a string representing a 'failregex'
|
||||
|
@ -102,9 +120,18 @@ Report bugs to https://github.com/fail2ban/fail2ban/issues
|
|||
version="%prog " + version)
|
||||
|
||||
p.add_options([
|
||||
Option("-d", "--datepattern",
|
||||
help="set custom pattern used to match date/times"),
|
||||
Option("-e", "--encoding",
|
||||
help="File encoding. Default: system locale"),
|
||||
Option("-L", "--maxlines", type=int, default=0,
|
||||
help="maxlines for multi-line regex"),
|
||||
Option("-m", "--journalmatch",
|
||||
help="journalctl style matches overriding filter file. "
|
||||
"\"systemd-journal\" only"),
|
||||
Option('-l', "--log-level", type="choice",
|
||||
dest="log_level",
|
||||
choices=('heavydebug', 'debug', 'info', 'warning', 'error', 'fatal'),
|
||||
choices=('heavydebug', 'debug', 'info', 'notice', 'warning', 'error', 'critical'),
|
||||
default=None,
|
||||
help="Log level for the Fail2Ban logger to use"),
|
||||
Option("-v", "--verbose", action='store_true',
|
||||
|
@ -177,8 +204,6 @@ class LineStats(object):
|
|||
|
||||
class Fail2banRegex(object):
|
||||
|
||||
CONFIG_DEFAULTS = {'configpath' : "/etc/fail2ban/"}
|
||||
|
||||
def __init__(self, opts):
|
||||
self._verbose = opts.verbose
|
||||
self._debuggex = opts.debuggex
|
||||
|
@ -187,34 +212,80 @@ class Fail2banRegex(object):
|
|||
self._print_no_ignored = opts.print_no_ignored
|
||||
self._print_all_missed = opts.print_all_missed
|
||||
self._print_all_ignored = opts.print_all_ignored
|
||||
self._maxlines_set = False # so we allow to override maxlines in cmdline
|
||||
self._datepattern_set = False
|
||||
self._journalmatch = None
|
||||
|
||||
self._filter = Filter(None)
|
||||
self._ignoreregex = list()
|
||||
self._failregex = list()
|
||||
self._line_stats = LineStats()
|
||||
|
||||
if opts.maxlines:
|
||||
self.setMaxLines(opts.maxlines)
|
||||
if opts.journalmatch is not None:
|
||||
self.setJournalMatch(opts.journalmatch.split())
|
||||
if opts.datepattern:
|
||||
self.setDatePattern(opts.datepattern)
|
||||
if opts.encoding:
|
||||
self.encoding = opts.encoding
|
||||
else:
|
||||
self.encoding = locale.getpreferredencoding()
|
||||
|
||||
|
||||
|
||||
def setDatePattern(self, pattern):
|
||||
if not self._datepattern_set:
|
||||
self._filter.setDatePattern(pattern)
|
||||
self._datepattern_set = True
|
||||
if pattern is not None:
|
||||
print "Use datepattern : %s" % (
|
||||
self._filter.getDatePattern()[1], )
|
||||
|
||||
def setMaxLines(self, v):
|
||||
if not self._maxlines_set:
|
||||
self._filter.setMaxLines(int(v))
|
||||
self._maxlines_set = True
|
||||
print "Use maxlines : %d" % self._filter.getMaxLines()
|
||||
|
||||
def setJournalMatch(self, v):
|
||||
if self._journalmatch is None:
|
||||
self._journalmatch = v
|
||||
|
||||
def readRegex(self, value, regextype):
|
||||
assert(regextype in ('fail', 'ignore'))
|
||||
regex = regextype + 'regex'
|
||||
if os.path.isfile(value):
|
||||
reader = SafeConfigParserWithIncludes(defaults=self.CONFIG_DEFAULTS)
|
||||
try:
|
||||
reader.read(value)
|
||||
print "Use %11s file : %s" % (regex, value)
|
||||
# TODO: reuse functionality in client
|
||||
print "Use %11s file : %s" % (regex, value)
|
||||
reader = FilterReader(value, 'fail2ban-regex-jail', {})
|
||||
reader.setBaseDir(None)
|
||||
|
||||
if reader.readexplicit():
|
||||
reader.getOptions(None)
|
||||
readercommands = reader.convert()
|
||||
regex_values = [
|
||||
RegexStat(m)
|
||||
for m in reader.get("Definition", regex).split('\n')
|
||||
if m != ""]
|
||||
except NoSectionError:
|
||||
print "No [Definition] section in %s" % value
|
||||
return False
|
||||
except NoOptionError:
|
||||
print "No %s option in %s" % (regex, value)
|
||||
return False
|
||||
except MissingSectionHeaderError:
|
||||
print "No section headers in %s" % value
|
||||
RegexStat(m[3])
|
||||
for m in filter(
|
||||
lambda x: x[0] == 'set' and x[2] == "add%sregex" % regextype,
|
||||
readercommands)]
|
||||
# Read out and set possible value of maxlines
|
||||
for command in readercommands:
|
||||
if command[2] == "maxlines":
|
||||
maxlines = int(command[3])
|
||||
try:
|
||||
self.setMaxLines(maxlines)
|
||||
except ValueError:
|
||||
print "ERROR: Invalid value for maxlines (%(maxlines)r) " \
|
||||
"read from %(value)s" % locals()
|
||||
return False
|
||||
elif command[2] == 'addjournalmatch':
|
||||
journalmatch = command[3]
|
||||
self.setJournalMatch(shlex.split(journalmatch))
|
||||
elif command[2] == 'datepattern':
|
||||
datepattern = command[3]
|
||||
self.setDatePattern(datepattern)
|
||||
else:
|
||||
print "ERROR: failed to read %s" % value
|
||||
return False
|
||||
else:
|
||||
print "Use %11s line : %s" % (regex, shortstr(value))
|
||||
|
@ -230,7 +301,7 @@ class Fail2banRegex(object):
|
|||
def testIgnoreRegex(self, line):
|
||||
found = False
|
||||
try:
|
||||
ret = self._filter.ignoreLine(line)
|
||||
ret = self._filter.ignoreLine([(line, "", "")])
|
||||
if ret is not None:
|
||||
found = True
|
||||
regex = self._ignoreregex[ret].inc()
|
||||
|
@ -239,9 +310,11 @@ class Fail2banRegex(object):
|
|||
return False
|
||||
return found
|
||||
|
||||
def testRegex(self, line):
|
||||
def testRegex(self, line, date=None):
|
||||
orgLineBuffer = self._filter._Filter__lineBuffer
|
||||
fullBuffer = len(orgLineBuffer) >= self._filter.getMaxLines()
|
||||
try:
|
||||
line, ret = self._filter.processLine(line, checkAllRegex=True)
|
||||
line, ret = self._filter.processLine(line, date, checkAllRegex=True)
|
||||
for match in ret:
|
||||
# Append True/False flag depending if line was matched by
|
||||
# more than one regex
|
||||
|
@ -255,17 +328,34 @@ class Fail2banRegex(object):
|
|||
except IndexError:
|
||||
print "Sorry, but no <HOST> found in regex"
|
||||
return False
|
||||
for bufLine in orgLineBuffer[int(fullBuffer):]:
|
||||
if bufLine not in self._filter._Filter__lineBuffer:
|
||||
try:
|
||||
self._line_stats.missed_lines.pop(
|
||||
self._line_stats.missed_lines.index("".join(bufLine)))
|
||||
self._line_stats.missed_lines_timeextracted.pop(
|
||||
self._line_stats.missed_lines_timeextracted.index(
|
||||
"".join(bufLine[::2])))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self._line_stats.matched += 1
|
||||
return line, ret
|
||||
|
||||
|
||||
def process(self, test_lines):
|
||||
|
||||
for line_no, line in enumerate(test_lines):
|
||||
if line.startswith('#') or not line.strip():
|
||||
# skip comment and empty lines
|
||||
continue
|
||||
is_ignored = fail2banRegex.testIgnoreRegex(line)
|
||||
line_datetimestripped, ret = fail2banRegex.testRegex(line)
|
||||
if isinstance(line, tuple):
|
||||
line_datetimestripped, ret = fail2banRegex.testRegex(
|
||||
line[0], line[1])
|
||||
line = "".join(line[0])
|
||||
else:
|
||||
line = line.rstrip('\r\n')
|
||||
if line.startswith('#') or not line:
|
||||
# skip comment and empty lines
|
||||
continue
|
||||
line_datetimestripped, ret = fail2banRegex.testRegex(line)
|
||||
is_ignored = fail2banRegex.testIgnoreRegex(line_datetimestripped)
|
||||
|
||||
if is_ignored:
|
||||
self._line_stats.ignored += 1
|
||||
|
@ -284,7 +374,7 @@ class Fail2banRegex(object):
|
|||
self._line_stats.missed_lines_timeextracted.append(line_datetimestripped)
|
||||
self._line_stats.tested += 1
|
||||
|
||||
if line_no % 10 == 0:
|
||||
if line_no % 10 == 0 and self._filter.dateDetector is not None:
|
||||
self._filter.dateDetector.sortTemplate()
|
||||
|
||||
|
||||
|
@ -339,7 +429,7 @@ class Fail2banRegex(object):
|
|||
" %s %s%s" % (
|
||||
ip[1],
|
||||
timeString,
|
||||
ip[3] and " (multiple regex matched)" or ""))
|
||||
ip[-1] and " (multiple regex matched)" or ""))
|
||||
|
||||
print "\n%s: %d total" % (title, total)
|
||||
pprint_list(out, " #) [# of hits] regular expression")
|
||||
|
@ -350,12 +440,14 @@ class Fail2banRegex(object):
|
|||
_ = print_failregexes("Ignoreregex", self._ignoreregex)
|
||||
|
||||
|
||||
print "\nDate template hits:"
|
||||
out = []
|
||||
for template in self._filter.dateDetector.getTemplates():
|
||||
if self._verbose or template.getHits():
|
||||
out.append("[%d] %s" % (template.getHits(), template.getName()))
|
||||
pprint_list(out, "[# of hits] date format")
|
||||
if self._filter.dateDetector is not None:
|
||||
print "\nDate template hits:"
|
||||
out = []
|
||||
for template in self._filter.dateDetector.templates:
|
||||
if self._verbose or template.hits:
|
||||
out.append("[%d] %s" % (
|
||||
template.hits, template.name))
|
||||
pprint_list(out, "[# of hits] date format")
|
||||
|
||||
print "\nLines: %s" % self._line_stats
|
||||
|
||||
|
@ -380,6 +472,11 @@ if __name__ == "__main__":
|
|||
parser.print_help()
|
||||
sys.exit(-1)
|
||||
|
||||
print
|
||||
print "Running tests"
|
||||
print "============="
|
||||
print
|
||||
|
||||
fail2banRegex = Fail2banRegex(opts)
|
||||
|
||||
# We need 2 or 3 parameters
|
||||
|
@ -394,9 +491,9 @@ if __name__ == "__main__":
|
|||
logSys.setLevel(getattr(logging, opts.log_level.upper()))
|
||||
else: # pragma: no cover
|
||||
# suppress the logging but it would leave unittests' progress dots
|
||||
# ticking, unless like with '-l fatal' which would be silent
|
||||
# ticking, unless like with '-l critical' which would be silent
|
||||
# unless error occurs
|
||||
logSys.setLevel(getattr(logging, 'FATAL'))
|
||||
logSys.setLevel(getattr(logging, 'CRITICAL'))
|
||||
|
||||
# Add the default logging handler
|
||||
stdout = logging.StreamHandler(sys.stdout)
|
||||
|
@ -410,33 +507,48 @@ if __name__ == "__main__":
|
|||
Formatter = logging.Formatter
|
||||
|
||||
# Custom log format for the verbose tests runs
|
||||
if opts.verbose > 1: # pragma: no cover
|
||||
if opts.verbose: # pragma: no cover
|
||||
stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt))
|
||||
else: # pragma: no cover
|
||||
# just prefix with the space
|
||||
stdout.setFormatter(Formatter(fmt))
|
||||
logSys.addHandler(stdout)
|
||||
|
||||
print
|
||||
print "Running tests"
|
||||
print "============="
|
||||
print
|
||||
|
||||
cmd_log, cmd_regex = args[:2]
|
||||
|
||||
fail2banRegex.readRegex(cmd_regex, 'fail') or sys.exit(-1)
|
||||
|
||||
if len(args) == 3:
|
||||
fail2banRegex.readRegex(args[2], 'ignore') or sys.exit(-1)
|
||||
|
||||
fail2banRegex.readRegex(cmd_regex, 'fail') or sys.exit(-1)
|
||||
|
||||
if os.path.isfile(cmd_log):
|
||||
try:
|
||||
hdlr = open(cmd_log)
|
||||
hdlr = open(cmd_log, 'rb')
|
||||
print "Use log file : %s" % cmd_log
|
||||
test_lines = hdlr # Iterable
|
||||
print "Use encoding : %s" % fail2banRegex.encoding
|
||||
test_lines = file_lines_gen(hdlr)
|
||||
except IOError, e:
|
||||
print e
|
||||
sys.exit(-1)
|
||||
elif cmd_log == "systemd-journal":
|
||||
if not journal:
|
||||
print "Error: systemd library not found. Exiting..."
|
||||
sys.exit(-1)
|
||||
myjournal = journal.Reader(converters={'__CURSOR': lambda x: x})
|
||||
journalmatch = fail2banRegex._journalmatch
|
||||
fail2banRegex.setDatePattern(None)
|
||||
if journalmatch:
|
||||
try:
|
||||
for element in journalmatch:
|
||||
if element == "+":
|
||||
myjournal.add_disjunction()
|
||||
else:
|
||||
myjournal.add_match(element)
|
||||
except ValueError:
|
||||
print "Error: Invalid journalmatch: %s" % shortstr(" ".join(journalmatch))
|
||||
sys.exit(-1)
|
||||
print "Use journal match : %s" % " ".join(journalmatch)
|
||||
test_lines = journal_lines_gen(myjournal)
|
||||
else:
|
||||
print "Use single line : %s" % shortstr(cmd_log)
|
||||
test_lines = [ cmd_log ]
|
|
@ -24,15 +24,8 @@ __license__ = "GPL"
|
|||
|
||||
import getopt, sys, logging, os
|
||||
|
||||
# Inserts our own modules path first in the list
|
||||
# fix for bug #343821
|
||||
try:
|
||||
from common.version import version
|
||||
except ImportError, e:
|
||||
sys.path.insert(1, "/usr/share/fail2ban")
|
||||
from common.version import version
|
||||
|
||||
from server.server import Server
|
||||
from fail2ban.version import version
|
||||
from fail2ban.server.server import Server
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban")
|
||||
|
@ -104,10 +97,10 @@ class Fail2banServer:
|
|||
if opt[0] == "-x":
|
||||
self.__conf["force"] = True
|
||||
if opt[0] in ["-h", "--help"]:
|
||||
self.dispUsage()
|
||||
self.dispUsage()
|
||||
sys.exit(0)
|
||||
if opt[0] in ["-V", "--version"]:
|
||||
self.dispVersion()
|
||||
self.dispVersion()
|
||||
sys.exit(0)
|
||||
|
||||
def start(self, argv):
|
|
@ -0,0 +1,128 @@
|
|||
#!/usr/bin/python
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
"""Script to run Fail2Ban tests battery
|
||||
"""
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012- Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
|
||||
import unittest, logging, sys, time, os
|
||||
|
||||
# Check if local fail2ban module exists, and use if it exists by
|
||||
# modifying the path. This is such that tests can be used in dev
|
||||
# environment.
|
||||
if os.path.exists("fail2ban/__init__.py"):
|
||||
sys.path.insert(0, ".")
|
||||
from fail2ban.version import version
|
||||
|
||||
from fail2ban.tests.utils import FormatterWithTraceBack, gatherTests
|
||||
from fail2ban.server.mytime import MyTime
|
||||
|
||||
from optparse import OptionParser, Option
|
||||
|
||||
def get_opt_parser():
|
||||
# use module docstring for help output
|
||||
p = OptionParser(
|
||||
usage="%s [OPTIONS] [regexps]\n" % sys.argv[0] + __doc__,
|
||||
version="%prog " + version)
|
||||
|
||||
p.add_options([
|
||||
Option('-l', "--log-level", type="choice",
|
||||
dest="log_level",
|
||||
choices=('heavydebug', 'debug', 'info', 'notice', 'warning', 'error', 'critical'),
|
||||
default=None,
|
||||
help="Log level for the logger to use during running tests"),
|
||||
Option('-n', "--no-network", action="store_true",
|
||||
dest="no_network",
|
||||
help="Do not run tests that require the network"),
|
||||
Option("-t", "--log-traceback", action='store_true',
|
||||
help="Enrich log-messages with compressed tracebacks"),
|
||||
Option("--full-traceback", action='store_true',
|
||||
help="Either to make the tracebacks full, not compressed (as by default)"),
|
||||
|
||||
])
|
||||
|
||||
return p
|
||||
|
||||
parser = get_opt_parser()
|
||||
(opts, regexps) = parser.parse_args()
|
||||
|
||||
#
|
||||
# Logging
|
||||
#
|
||||
logSys = logging.getLogger("fail2ban")
|
||||
|
||||
# Numerical level of verbosity corresponding to a log "level"
|
||||
verbosity = {'heavydebug': 4,
|
||||
'debug': 3,
|
||||
'info': 2,
|
||||
'notice': 2,
|
||||
'warning': 1,
|
||||
'error': 1,
|
||||
'critical': 0,
|
||||
None: 1}[opts.log_level]
|
||||
|
||||
if opts.log_level is not None: # pragma: no cover
|
||||
# so we had explicit settings
|
||||
logSys.setLevel(getattr(logging, opts.log_level.upper()))
|
||||
else: # pragma: no cover
|
||||
# suppress the logging but it would leave unittests' progress dots
|
||||
# ticking, unless like with '-l critical' which would be silent
|
||||
# unless error occurs
|
||||
logSys.setLevel(getattr(logging, 'CRITICAL'))
|
||||
|
||||
# Add the default logging handler
|
||||
stdout = logging.StreamHandler(sys.stdout)
|
||||
|
||||
fmt = ' %(message)s'
|
||||
|
||||
if opts.log_traceback:
|
||||
Formatter = FormatterWithTraceBack
|
||||
fmt = (opts.full_traceback and ' %(tb)s' or ' %(tbc)s') + fmt
|
||||
else:
|
||||
Formatter = logging.Formatter
|
||||
|
||||
# Custom log format for the verbose tests runs
|
||||
if verbosity > 1: # pragma: no cover
|
||||
stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt))
|
||||
else: # pragma: no cover
|
||||
# just prefix with the space
|
||||
stdout.setFormatter(Formatter(fmt))
|
||||
logSys.addHandler(stdout)
|
||||
|
||||
#
|
||||
# Let know the version
|
||||
#
|
||||
if not opts.log_level or opts.log_level != 'critical': # pragma: no cover
|
||||
print("Fail2ban %s test suite. Python %s. Please wait..." \
|
||||
% (version, str(sys.version).replace('\n', '')))
|
||||
|
||||
tests = gatherTests(regexps, opts.no_network)
|
||||
#
|
||||
# Run the tests
|
||||
#
|
||||
testRunner = unittest.TextTestRunner(verbosity=verbosity)
|
||||
|
||||
tests_results = testRunner.run(tests)
|
||||
|
||||
if not tests_results.wasSuccessful(): # pragma: no cover
|
||||
sys.exit(1)
|
|
@ -1,90 +0,0 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging
|
||||
from configreader import ConfigReader
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
|
||||
class ActionReader(ConfigReader):
|
||||
|
||||
def __init__(self, action, name, **kwargs):
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
self.__file = action[0]
|
||||
self.__cInfo = action[1]
|
||||
self.__name = name
|
||||
|
||||
def setFile(self, fileName):
|
||||
self.__file = 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, "action.d/" + self.__file)
|
||||
|
||||
def getOptions(self, pOpts):
|
||||
opts = [["string", "actionstart", ""],
|
||||
["string", "actionstop", ""],
|
||||
["string", "actioncheck", ""],
|
||||
["string", "actionban", ""],
|
||||
["string", "actionunban", ""]]
|
||||
self.__opts = ConfigReader.getOptions(self, "Definition", opts, pOpts)
|
||||
|
||||
if self.has_section("Init"):
|
||||
for opt in self.options("Init"):
|
||||
if not self.__cInfo.has_key(opt):
|
||||
self.__cInfo[opt] = self.get("Init", opt)
|
||||
|
||||
def convert(self):
|
||||
head = ["set", self.__name]
|
||||
stream = list()
|
||||
stream.append(head + ["addaction", self.__file])
|
||||
for opt in self.__opts:
|
||||
if opt == "actionstart":
|
||||
stream.append(head + ["actionstart", self.__file, self.__opts[opt]])
|
||||
elif opt == "actionstop":
|
||||
stream.append(head + ["actionstop", self.__file, self.__opts[opt]])
|
||||
elif opt == "actioncheck":
|
||||
stream.append(head + ["actioncheck", self.__file, self.__opts[opt]])
|
||||
elif opt == "actionban":
|
||||
stream.append(head + ["actionban", self.__file, self.__opts[opt]])
|
||||
elif opt == "actionunban":
|
||||
stream.append(head + ["actionunban", self.__file, self.__opts[opt]])
|
||||
# cInfo
|
||||
if self.__cInfo:
|
||||
for p in self.__cInfo:
|
||||
stream.append(head + ["setcinfo", self.__file, p, self.__cInfo[p]])
|
||||
|
||||
return stream
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import os
|
||||
import logging
|
||||
from configreader import ConfigReader
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
|
||||
class FilterReader(ConfigReader):
|
||||
|
||||
def __init__(self, fileName, name, **kwargs):
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
# Defer initialization to the set Methods
|
||||
self.__file = self.__name = self.__opts = None
|
||||
self.setFile(fileName)
|
||||
self.setName(name)
|
||||
|
||||
def setFile(self, fileName):
|
||||
self.__file = fileName
|
||||
self.__opts = None
|
||||
|
||||
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("filter.d", self.__file))
|
||||
|
||||
def getOptions(self, pOpts):
|
||||
opts = [["string", "ignoreregex", ""],
|
||||
["string", "failregex", ""],
|
||||
]
|
||||
self.__opts = ConfigReader.getOptions(self, "Definition", opts, pOpts)
|
||||
|
||||
def convert(self):
|
||||
stream = list()
|
||||
for opt in self.__opts:
|
||||
if opt == "failregex":
|
||||
for regex in self.__opts[opt].split('\n'):
|
||||
# Do not send a command if the rule is empty.
|
||||
if regex != '':
|
||||
stream.append(["set", self.__name, "addfailregex", regex])
|
||||
elif opt == "ignoreregex":
|
||||
for regex in self.__opts[opt].split('\n'):
|
||||
# Do not send a command if the rule is empty.
|
||||
if regex != '':
|
||||
stream.append(["set", self.__name, "addignoreregex", regex])
|
||||
return stream
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
if sys.version_info < (2, 7):
|
||||
raise ImportError("badips.py action requires Python >= 2.7")
|
||||
import json
|
||||
from functools import partial
|
||||
import threading
|
||||
import logging
|
||||
if sys.version_info >= (3, ):
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.parse import urlencode
|
||||
from urllib.error import HTTPError
|
||||
else:
|
||||
from urllib2 import Request, urlopen, HTTPError
|
||||
from urllib import urlencode
|
||||
|
||||
from fail2ban.server.actions import ActionBase
|
||||
from fail2ban.version import version as f2bVersion
|
||||
|
||||
class BadIPsAction(ActionBase):
|
||||
"""Fail2Ban action which resports bans to badips.com, and also
|
||||
blacklist bad IPs listed on badips.com by using another action's
|
||||
ban method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail which the action belongs to.
|
||||
name : str
|
||||
Name assigned to the action.
|
||||
category : str
|
||||
Valid badips.com category for reporting failures.
|
||||
score : int, optional
|
||||
Minimum score for bad IPs. Default 3.
|
||||
age : str, optional
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
Default "24h" (24 hours)
|
||||
key : str, optional
|
||||
Key issued by badips.com to report bans, for later retrieval
|
||||
of personalised content.
|
||||
banaction : str, optional
|
||||
Name of banaction to use for blacklisting bad IPs. If `None`,
|
||||
no blacklist of IPs will take place.
|
||||
Default `None`.
|
||||
bancategory : str, optional
|
||||
Name of category to use for blacklisting, which can differ
|
||||
from category used for reporting. e.g. may want to report
|
||||
"postfix", but want to use whole "mail" category for blacklist.
|
||||
Default `category`.
|
||||
bankey : str, optional
|
||||
Key issued by badips.com to blacklist IPs reported with the
|
||||
associated key.
|
||||
updateperiod : int, optional
|
||||
Time in seconds between updating bad IPs blacklist.
|
||||
Default 900 (15 minutes)
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If invalid `category`, `score`, `banaction` or `updateperiod`.
|
||||
"""
|
||||
|
||||
_badips = "http://www.badips.com"
|
||||
_Request = partial(
|
||||
Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion})
|
||||
|
||||
def __init__(self, jail, name, category, score=3, age="24h", key=None,
|
||||
banaction=None, bancategory=None, bankey=None, updateperiod=900):
|
||||
super(BadIPsAction, self).__init__(jail, name)
|
||||
|
||||
self.category = category
|
||||
self.score = score
|
||||
self.age = age
|
||||
self.key = key
|
||||
self.banaction = banaction
|
||||
self.bancategory = bancategory or category
|
||||
self.bankey = bankey
|
||||
self.updateperiod = updateperiod
|
||||
|
||||
self._bannedips = set()
|
||||
# Used later for threading.Timer for updating badips
|
||||
self._timer = None
|
||||
|
||||
@classmethod
|
||||
def getCategories(cls, incParents=False):
|
||||
"""Get badips.com categories.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of categories.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
response = urlopen(
|
||||
cls._Request("/".join([cls._badips, "get", "categories"])))
|
||||
except HTTPError as response:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.error(
|
||||
"Failed to fetch categories. badips.com response: '%s'",
|
||||
messages['err'])
|
||||
raise
|
||||
else:
|
||||
categories = json.loads(response.read().decode('utf-8'))['categories']
|
||||
categories_names = set(
|
||||
value['Name'] for value in categories)
|
||||
if incParents:
|
||||
categories_names.update(set(
|
||||
value['Parent'] for value in categories
|
||||
if "Parent" in value))
|
||||
return categories_names
|
||||
|
||||
@classmethod
|
||||
def getList(cls, category, score, age, key=None):
|
||||
"""Get badips.com list of bad IPs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
category : str
|
||||
Valid badips.com category.
|
||||
score : int
|
||||
Minimum score for bad IPs.
|
||||
age : str
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
key : str, optional
|
||||
Key issued by badips.com to fetch IPs reported with the
|
||||
associated key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of bad IPs.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "?".join([
|
||||
"/".join([cls._badips, "get", "list", category, str(score)]),
|
||||
urlencode({'age': age})])
|
||||
if key:
|
||||
url = "&".join([url, urlencode({"key", key})])
|
||||
response = urlopen(cls._Request(url))
|
||||
except HTTPError as response:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.error(
|
||||
"Failed to fetch bad IP list. badips.com response: '%s'",
|
||||
messages['err'])
|
||||
raise
|
||||
else:
|
||||
return set(response.read().decode('utf-8').split())
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
"""badips.com category for reporting IPs.
|
||||
"""
|
||||
return self._category
|
||||
|
||||
@category.setter
|
||||
def category(self, category):
|
||||
if category not in self.getCategories():
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
category)
|
||||
raise ValueError("Invalid category: %s" % category)
|
||||
self._category = category
|
||||
|
||||
@property
|
||||
def bancategory(self):
|
||||
"""badips.com bancategory for fetching IPs.
|
||||
"""
|
||||
return self._bancategory
|
||||
|
||||
@bancategory.setter
|
||||
def bancategory(self, bancategory):
|
||||
if bancategory not in self.getCategories(incParents=True):
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
bancategory)
|
||||
raise ValueError("Invalid bancategory: %s" % bancategory)
|
||||
self._bancategory = bancategory
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
"""badips.com minimum score for fetching IPs.
|
||||
"""
|
||||
return self._score
|
||||
|
||||
@score.setter
|
||||
def score(self, score):
|
||||
score = int(score)
|
||||
if 0 <= score <= 5:
|
||||
self._score = score
|
||||
else:
|
||||
raise ValueError("Score must be 0-5")
|
||||
|
||||
@property
|
||||
def banaction(self):
|
||||
"""Jail action to use for banning/unbanning.
|
||||
"""
|
||||
return self._banaction
|
||||
|
||||
@banaction.setter
|
||||
def banaction(self, banaction):
|
||||
if banaction is not None and banaction not in self._jail.actions:
|
||||
self._logSys.error("Action name '%s' not in jail '%s'",
|
||||
banaction, self._jail.name)
|
||||
raise ValueError("Invalid banaction")
|
||||
self._banaction = banaction
|
||||
|
||||
@property
|
||||
def updateperiod(self):
|
||||
"""Period in seconds between banned bad IPs will be updated.
|
||||
"""
|
||||
return self._updateperiod
|
||||
|
||||
@updateperiod.setter
|
||||
def updateperiod(self, updateperiod):
|
||||
updateperiod = int(updateperiod)
|
||||
if updateperiod > 0:
|
||||
self._updateperiod = updateperiod
|
||||
else:
|
||||
raise ValueError("Update period must be integer greater than 0")
|
||||
|
||||
def _banIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
self._jail.actions[self.banaction].ban({
|
||||
'ip': ip,
|
||||
'failures': 0,
|
||||
'matches': "",
|
||||
'ipmatches': "",
|
||||
'ipjailmatches': "",
|
||||
})
|
||||
except Exception as e:
|
||||
self._logSys.error(
|
||||
"Error banning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel<=logging.DEBUG)
|
||||
else:
|
||||
self._bannedips.add(ip)
|
||||
self._logSys.info(
|
||||
"Banned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
|
||||
def _unbanIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
self._jail.actions[self.banaction].unban({
|
||||
'ip': ip,
|
||||
'failures': 0,
|
||||
'matches': "",
|
||||
'ipmatches': "",
|
||||
'ipjailmatches': "",
|
||||
})
|
||||
except Exception as e:
|
||||
self._logSys.info(
|
||||
"Error unbanning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel<=logging.DEBUG)
|
||||
else:
|
||||
self._logSys.info(
|
||||
"Unbanned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
finally:
|
||||
self._bannedips.remove(ip)
|
||||
|
||||
def start(self):
|
||||
"""If `banaction` set, blacklists bad IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""If `banaction` set, updates blacklisted IPs.
|
||||
|
||||
Queries badips.com for list of bad IPs, removing IPs from the
|
||||
blacklist if no longer present, and adds new bad IPs to the
|
||||
blacklist.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
|
||||
try:
|
||||
ips = self.getList(
|
||||
self.bancategory, self.score, self.age, self.bankey)
|
||||
# Remove old IPs no longer listed
|
||||
self._unbanIPs(self._bannedips - ips)
|
||||
# Add new IPs which are now listed
|
||||
self._banIPs(ips - self._bannedips)
|
||||
|
||||
self._logSys.info(
|
||||
"Updated IPs for jail '%s'. Update again in %i seconds",
|
||||
self._jail.name, self.updateperiod)
|
||||
finally:
|
||||
self._timer = threading.Timer(self.updateperiod, self.update)
|
||||
self._timer.start()
|
||||
|
||||
def stop(self):
|
||||
"""If `banaction` set, clears blacklisted IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
self._unbanIPs(self._bannedips.copy())
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Reports banned IP to badips.com.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "/".join([self._badips, "add", self.category, aInfo['ip']])
|
||||
if self.key:
|
||||
url = "?".join([url, urlencode({"key", self.key})])
|
||||
response = urlopen(self._Request(url))
|
||||
except HTTPError as response:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.error(
|
||||
"Response from badips.com report: '%s'",
|
||||
messages['err'])
|
||||
raise
|
||||
else:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.info(
|
||||
"Response from badips.com report: '%s'",
|
||||
messages['suc'])
|
||||
|
||||
Action = BadIPsAction
|
|
@ -8,19 +8,19 @@ before = iptables-blocktype.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
actionstart = firewall-cmd --direct --add-chain ipv4 filter fail2ban-<name>
|
||||
firewall-cmd --direct --add-rule ipv4 filter fail2ban-<name> 1000 -j RETURN
|
||||
firewall-cmd --direct --add-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name>
|
||||
actionstart = firewall-cmd --direct --add-chain ipv4 filter f2b-<name>
|
||||
firewall-cmd --direct --add-rule ipv4 filter f2b-<name> 1000 -j RETURN
|
||||
firewall-cmd --direct --add-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
|
||||
|
||||
actionstop = firewall-cmd --direct --remove-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name>
|
||||
firewall-cmd --direct --remove-rules ipv4 filter fail2ban-<name>
|
||||
firewall-cmd --direct --remove-chain ipv4 filter fail2ban-<name>
|
||||
actionstop = firewall-cmd --direct --remove-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
|
||||
firewall-cmd --direct --remove-rules ipv4 filter f2b-<name>
|
||||
firewall-cmd --direct --remove-chain ipv4 filter f2b-<name>
|
||||
|
||||
actioncheck = firewall-cmd --direct --get-chains ipv4 filter | grep -q '^fail2ban-<name>$'
|
||||
actioncheck = firewall-cmd --direct --get-chains ipv4 filter | grep -q 'f2b-<name>$'
|
||||
|
||||
actionban = firewall-cmd --direct --add-rule ipv4 filter fail2ban-<name> 0 -s <ip> -j <blocktype>
|
||||
actionban = firewall-cmd --direct --add-rule ipv4 filter f2b-<name> 0 -s <ip> -j <blocktype>
|
||||
|
||||
actionunban = firewall-cmd --direct --remove-rule ipv4 filter fail2ban-<name> 0 -s <ip> -j <blocktype>
|
||||
actionunban = firewall-cmd --direct --remove-rule ipv4 filter f2b-<name> 0 -s <ip> -j <blocktype>
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -17,23 +17,23 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = iptables -N fail2ban-<name>
|
||||
iptables -A fail2ban-<name> -j RETURN
|
||||
iptables -I <chain> -p <protocol> -j fail2ban-<name>
|
||||
actionstart = iptables -N f2b-<name>
|
||||
iptables -A f2b-<name> -j RETURN
|
||||
iptables -I <chain> -p <protocol> -j f2b-<name>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D <chain> -p <protocol> -j fail2ban-<name>
|
||||
iptables -F fail2ban-<name>
|
||||
iptables -X fail2ban-<name>
|
||||
actionstop = iptables -D <chain> -p <protocol> -j f2b-<name>
|
||||
iptables -F f2b-<name>
|
||||
iptables -X f2b-<name>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -41,7 +41,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
||||
actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -49,7 +49,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype>
|
||||
actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -27,16 +27,16 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = ipset --create fail2ban-<name> iphash
|
||||
iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype>
|
||||
actionstart = ipset --create f2b-<name> iphash
|
||||
iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype>
|
||||
ipset --flush fail2ban-<name>
|
||||
ipset --destroy fail2ban-<name>
|
||||
actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
|
||||
ipset --flush f2b-<name>
|
||||
ipset --destroy f2b-<name>
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -44,7 +44,7 @@ actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = ipset --test fail2ban-<name> <ip> || ipset --add fail2ban-<name> <ip>
|
||||
actionban = ipset --test f2b-<name> <ip> || ipset --add f2b-<name> <ip>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -52,7 +52,7 @@ actionban = ipset --test fail2ban-<name> <ip> || ipset --add fail2ban-<name> <i
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = ipset --test fail2ban-<name> <ip> && ipset --del fail2ban-<name> <ip>
|
||||
actionunban = ipset --test f2b-<name> <ip> && ipset --del f2b-<name> <ip>
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -24,16 +24,16 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = ipset create fail2ban-<name> hash:ip timeout <bantime>
|
||||
iptables -I INPUT -m set --match-set fail2ban-<name> src -j <blocktype>
|
||||
actionstart = ipset create f2b-<name> hash:ip timeout <bantime>
|
||||
iptables -I INPUT -m set --match-set f2b-<name> src -j <blocktype>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D INPUT -m set --match-set fail2ban-<name> src -j <blocktype>
|
||||
ipset flush fail2ban-<name>
|
||||
ipset destroy fail2ban-<name>
|
||||
actionstop = iptables -D INPUT -m set --match-set f2b-<name> src -j <blocktype>
|
||||
ipset flush f2b-<name>
|
||||
ipset destroy f2b-<name>
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -41,7 +41,7 @@ actionstop = iptables -D INPUT -m set --match-set fail2ban-<name> src -j <blockt
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist
|
||||
actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -49,7 +49,7 @@ actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = ipset del fail2ban-<name> <ip> -exist
|
||||
actionunban = ipset del f2b-<name> <ip> -exist
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -24,16 +24,16 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = ipset create fail2ban-<name> hash:ip timeout <bantime>
|
||||
iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype>
|
||||
actionstart = ipset create f2b-<name> hash:ip timeout <bantime>
|
||||
iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype>
|
||||
ipset flush fail2ban-<name>
|
||||
ipset destroy fail2ban-<name>
|
||||
actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
|
||||
ipset flush f2b-<name>
|
||||
ipset destroy f2b-<name>
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -41,7 +41,7 @@ actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist
|
||||
actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -49,7 +49,7 @@ actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = ipset del fail2ban-<name> <ip> -exist
|
||||
actionunban = ipset del f2b-<name> <ip> -exist
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
# Author: Guido Bozzetto
|
||||
# Modified: Cyril Jaquier
|
||||
#
|
||||
# make "fail2ban-<name>" chain to match drop IP
|
||||
# make "fail2ban-<name>-log" chain to log and drop
|
||||
# insert a jump to fail2ban-<name> from -I <chain> if proto/port match
|
||||
# make "f2b-<name>" chain to match drop IP
|
||||
# make "f2b-<name>-log" chain to log and drop
|
||||
# insert a jump to f2b-<name> from -I <chain> if proto/port match
|
||||
#
|
||||
#
|
||||
|
||||
|
@ -19,28 +19,28 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = iptables -N fail2ban-<name>
|
||||
iptables -A fail2ban-<name> -j RETURN
|
||||
iptables -I <chain> 1 -p <protocol> -m multiport --dports <port> -j fail2ban-<name>
|
||||
iptables -N fail2ban-<name>-log
|
||||
iptables -I fail2ban-<name>-log -j LOG --log-prefix "$(expr fail2ban-<name> : '\(.\{1,23\}\)'):DROP " --log-level warning -m limit --limit 6/m --limit-burst 2
|
||||
iptables -A fail2ban-<name>-log -j <blocktype>
|
||||
actionstart = iptables -N f2b-<name>
|
||||
iptables -A f2b-<name> -j RETURN
|
||||
iptables -I <chain> 1 -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
iptables -N f2b-<name>-log
|
||||
iptables -I f2b-<name>-log -j LOG --log-prefix "$(expr f2b-<name> : '\(.\{1,23\}\)'):DROP " --log-level warning -m limit --limit 6/m --limit-burst 2
|
||||
iptables -A f2b-<name>-log -j <blocktype>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name>
|
||||
iptables -F fail2ban-<name>
|
||||
iptables -F fail2ban-<name>-log
|
||||
iptables -X fail2ban-<name>
|
||||
iptables -X fail2ban-<name>-log
|
||||
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
iptables -F f2b-<name>
|
||||
iptables -F f2b-<name>-log
|
||||
iptables -X f2b-<name>
|
||||
iptables -X f2b-<name>-log
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck = iptables -n -L fail2ban-<name>-log >/dev/null
|
||||
actioncheck = iptables -n -L f2b-<name>-log >/dev/null
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -48,7 +48,7 @@ actioncheck = iptables -n -L fail2ban-<name>-log >/dev/null
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j fail2ban-<name>-log
|
||||
actionban = iptables -I f2b-<name> 1 -s <ip> -j f2b-<name>-log
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -56,7 +56,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j fail2ban-<name>-log
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = iptables -D fail2ban-<name> -s <ip> -j fail2ban-<name>-log
|
||||
actionunban = iptables -D f2b-<name> -s <ip> -j f2b-<name>-log
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -14,23 +14,23 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = iptables -N fail2ban-<name>
|
||||
iptables -A fail2ban-<name> -j RETURN
|
||||
iptables -I <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name>
|
||||
actionstart = iptables -N f2b-<name>
|
||||
iptables -A f2b-<name> -j RETURN
|
||||
iptables -I <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name>
|
||||
iptables -F fail2ban-<name>
|
||||
iptables -X fail2ban-<name>
|
||||
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
iptables -F f2b-<name>
|
||||
iptables -X f2b-<name>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -38,7 +38,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
||||
actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -46,7 +46,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype>
|
||||
actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -17,23 +17,23 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = iptables -N fail2ban-<name>
|
||||
iptables -A fail2ban-<name> -j RETURN
|
||||
iptables -I <chain> -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name>
|
||||
actionstart = iptables -N f2b-<name>
|
||||
iptables -A f2b-<name> -j RETURN
|
||||
iptables -I <chain> -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D <chain> -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name>
|
||||
iptables -F fail2ban-<name>
|
||||
iptables -X fail2ban-<name>
|
||||
actionstop = iptables -D <chain> -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
|
||||
iptables -F f2b-<name>
|
||||
iptables -X f2b-<name>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -41,7 +41,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
||||
actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -49,7 +49,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype>
|
||||
actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -23,30 +23,30 @@ before = iptables-blocktype.conf
|
|||
# iptables-persistent package).
|
||||
#
|
||||
# Explanation of the rule below:
|
||||
# Check if any packets coming from an IP on the fail2ban-<name>
|
||||
# Check if any packets coming from an IP on the f2b-<name>
|
||||
# list have been seen in the last 3600 seconds. If yes, update the
|
||||
# timestamp for this IP and drop the packet. If not, let the packet
|
||||
# through.
|
||||
#
|
||||
# Fail2ban inserts blacklisted hosts into the fail2ban-<name> list
|
||||
# Fail2ban inserts blacklisted hosts into the f2b-<name> list
|
||||
# and removes them from the list after some time, according to its
|
||||
# own rules. The 3600 second timeout is independent and acts as a
|
||||
# safeguard in case the fail2ban process dies unexpectedly. The
|
||||
# shorter of the two timeouts actually matters.
|
||||
actionstart = if [ `id -u` -eq 0 ];then iptables -I INPUT -m recent --update --seconds 3600 --name fail2ban-<name> -j <blocktype>;fi
|
||||
actionstart = if [ `id -u` -eq 0 ];then iptables -I INPUT -m recent --update --seconds 3600 --name f2b-<name> -j <blocktype>;fi
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = echo / > /proc/net/xt_recent/fail2ban-<name>
|
||||
if [ `id -u` -eq 0 ];then iptables -D INPUT -m recent --update --seconds 3600 --name fail2ban-<name> -j <blocktype>;fi
|
||||
actionstop = echo / > /proc/net/xt_recent/f2b-<name>
|
||||
if [ `id -u` -eq 0 ];then iptables -D INPUT -m recent --update --seconds 3600 --name f2b-<name> -j <blocktype>;fi
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck = test -e /proc/net/xt_recent/fail2ban-<name>
|
||||
actioncheck = test -e /proc/net/xt_recent/f2b-<name>
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -54,7 +54,7 @@ actioncheck = test -e /proc/net/xt_recent/fail2ban-<name>
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = echo +<ip> > /proc/net/xt_recent/fail2ban-<name>
|
||||
actionban = echo +<ip> > /proc/net/xt_recent/f2b-<name>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -62,7 +62,7 @@ actionban = echo +<ip> > /proc/net/xt_recent/fail2ban-<name>
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = echo -<ip> > /proc/net/xt_recent/fail2ban-<name>
|
||||
actionunban = echo -<ip> > /proc/net/xt_recent/f2b-<name>
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -14,23 +14,23 @@ before = iptables-blocktype.conf
|
|||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = iptables -N fail2ban-<name>
|
||||
iptables -A fail2ban-<name> -j RETURN
|
||||
iptables -I <chain> -p <protocol> --dport <port> -j fail2ban-<name>
|
||||
actionstart = iptables -N f2b-<name>
|
||||
iptables -A f2b-<name> -j RETURN
|
||||
iptables -I <chain> -p <protocol> --dport <port> -j f2b-<name>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = iptables -D <chain> -p <protocol> --dport <port> -j fail2ban-<name>
|
||||
iptables -F fail2ban-<name>
|
||||
iptables -X fail2ban-<name>
|
||||
actionstop = iptables -D <chain> -p <protocol> --dport <port> -j f2b-<name>
|
||||
iptables -F f2b-<name>
|
||||
iptables -X f2b-<name>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
||||
actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
|
@ -38,7 +38,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
||||
actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
@ -46,7 +46,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
|
|||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype>
|
||||
actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
|
||||
|
||||
[Init]
|
||||
|
||||
|
|
|
@ -8,6 +8,56 @@
|
|||
|
||||
after = sendmail-common.local
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = printf %%b "Subject: [Fail2Ban] <name>: started on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = printf %%b "Subject: [Fail2Ban] <name>: stopped on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# 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 =
|
||||
|
||||
# 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]
|
||||
|
||||
# Recipient mail address
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# Fail2Ban configuration file
|
||||
#
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = sendmail-common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
# 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 = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The IP <ip> has just been banned by Fail2Ban after
|
||||
<failures> attempts against <name>.\n\n
|
||||
Here are more information about <ip>:\n
|
||||
`/usr/bin/whois <ip>`\n\n
|
||||
Matches for <name> with <ipjailfailures> failures IP:<ip>\n
|
||||
<ipjailmatches>\n\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
[Init]
|
||||
|
||||
# Default name of the chain
|
||||
#
|
||||
name = default
|
|
@ -0,0 +1,37 @@
|
|||
# Fail2Ban configuration file
|
||||
#
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = sendmail-common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
# 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 = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The IP <ip> has just been banned by Fail2Ban after
|
||||
<failures> attempts against <name>.\n\n
|
||||
Here are more information about <ip>:\n
|
||||
`/usr/bin/whois <ip>`\n\n
|
||||
Matches with <ipfailures> failures IP:<ip>\n
|
||||
<ipmatches>\n\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
[Init]
|
||||
|
||||
# Default name of the chain
|
||||
#
|
||||
name = default
|
|
@ -10,38 +10,6 @@ before = sendmail-common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = printf %%b "Subject: [Fail2Ban] <name>: started on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = printf %%b "Subject: [Fail2Ban] <name>: stopped on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# 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.
|
||||
|
@ -62,14 +30,6 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from `uname -n`
|
|||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# 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]
|
||||
|
||||
# Default name of the chain
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# Fail2Ban configuration file
|
||||
#
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = sendmail-common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
# 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 = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The IP <ip> has just been banned by Fail2Ban after
|
||||
<failures> attempts against <name>.\n\n
|
||||
Here are more information about <ip>:\n
|
||||
`/usr/bin/whois <ip>`\n\n
|
||||
Matches:\n
|
||||
<matches>\n\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
[Init]
|
||||
|
||||
# Default name of the chain
|
||||
#
|
||||
name = default
|
|
@ -10,38 +10,6 @@ before = sendmail-common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = printf %%b "Subject: [Fail2Ban] <name>: started on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = printf %%b "Subject: [Fail2Ban] <name>: stopped on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# 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.
|
||||
|
@ -60,14 +28,6 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from `uname -n`
|
|||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# 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]
|
||||
|
||||
# Default name of the chain
|
||||
|
|
|
@ -10,38 +10,6 @@ before = sendmail-common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = printf %%b "Subject: [Fail2Ban] <name>: started on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = printf %%b "Subject: [Fail2Ban] <name>: stopped on `uname -n`
|
||||
Date: `LC_TIME=C date -u +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
From: <sendername> <<sender>>
|
||||
To: <dest>\n
|
||||
Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# 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.
|
||||
|
@ -58,14 +26,6 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from `uname -n`
|
|||
Regards,\n
|
||||
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
|
||||
|
||||
# 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]
|
||||
|
||||
# Default name of the chain
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, formataddr
|
||||
|
||||
from fail2ban.server.actions import ActionBase, CallingMap
|
||||
|
||||
messages = {}
|
||||
messages['start'] = \
|
||||
"""Hi,
|
||||
|
||||
The jail %(jailname)s has been started successfully.
|
||||
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
|
||||
messages['stop'] = \
|
||||
"""Hi,
|
||||
|
||||
The jail %(jailname)s has been stopped.
|
||||
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
|
||||
messages['ban'] = {}
|
||||
messages['ban']['head'] = \
|
||||
"""Hi,
|
||||
|
||||
The IP %(ip)s has just been banned for %(bantime)s seconds
|
||||
by Fail2Ban after %(failures)i attempts against %(jailname)s.
|
||||
"""
|
||||
messages['ban']['tail'] = \
|
||||
"""
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
messages['ban']['matches'] = \
|
||||
"""
|
||||
Matches for this ban:
|
||||
%(matches)s
|
||||
"""
|
||||
messages['ban']['ipmatches'] = \
|
||||
"""
|
||||
Matches for %(ip)s:
|
||||
%(ipmatches)s
|
||||
"""
|
||||
messages['ban']['ipjailmatches'] = \
|
||||
"""
|
||||
Matches for %(ip)s for jail %(jailname)s:
|
||||
%(ipjailmatches)s
|
||||
"""
|
||||
|
||||
class SMTPAction(ActionBase):
|
||||
"""Fail2Ban action which sends emails to inform on jail starting,
|
||||
stopping and bans.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, jail, name, host="localhost", user=None, password=None,
|
||||
sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None):
|
||||
"""Initialise action.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail which the action belongs to.
|
||||
name : str
|
||||
Named assigned to the action.
|
||||
host : str, optional
|
||||
SMTP host, of host:port format. Default host "localhost" and
|
||||
port "25"
|
||||
user : str, optional
|
||||
Username used for authentication with SMTP server.
|
||||
password : str, optional
|
||||
Password used for authentication with SMTP server.
|
||||
sendername : str, optional
|
||||
Name to use for from address in email. Default "Fail2Ban".
|
||||
sender : str, optional
|
||||
Email address to use for from address in email.
|
||||
Default "fail2ban".
|
||||
dest : str, optional
|
||||
Email addresses of intended recipient(s) in comma space ", "
|
||||
delimited format. Default "root".
|
||||
matches : str, optional
|
||||
Type of matches to be included from ban in email. Can be one
|
||||
of "matches", "ipmatches" or "ipjailmatches". Default None
|
||||
(see man jail.conf.5).
|
||||
"""
|
||||
|
||||
super(SMTPAction, self).__init__(jail, name)
|
||||
|
||||
self.host = host
|
||||
#TODO: self.ssl = ssl
|
||||
|
||||
self.user = user
|
||||
self.password =password
|
||||
|
||||
self.fromname = sendername
|
||||
self.fromaddr = sender
|
||||
self.toaddr = dest
|
||||
|
||||
self.matches = matches
|
||||
|
||||
self.message_values = CallingMap(
|
||||
jailname = self._jail.name,
|
||||
hostname = socket.gethostname,
|
||||
bantime = self._jail.actions.getBanTime,
|
||||
)
|
||||
|
||||
def _sendMessage(self, subject, text):
|
||||
"""Sends message based on arguments and instance's properties.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subject : str
|
||||
Subject of the email.
|
||||
text : str
|
||||
Body of the email.
|
||||
|
||||
Raises
|
||||
------
|
||||
SMTPConnectionError
|
||||
Error on connecting to host.
|
||||
SMTPAuthenticationError
|
||||
Error authenticating with SMTP server.
|
||||
SMTPException
|
||||
See Python `smtplib` for full list of other possible
|
||||
exceptions.
|
||||
"""
|
||||
msg = MIMEText(text)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = formataddr((self.fromname, self.fromaddr))
|
||||
msg['To'] = self.toaddr
|
||||
msg['Date'] = formatdate()
|
||||
|
||||
smtp = smtplib.SMTP()
|
||||
try:
|
||||
self._logSys.debug("Connected to SMTP '%s', response: %i: %s",
|
||||
self.host, *smtp.connect(self.host))
|
||||
if self.user and self.password:
|
||||
smtp.login(self.user, self.password)
|
||||
failed_recipients = smtp.sendmail(
|
||||
self.fromaddr, self.toaddr.split(", "), msg.as_string())
|
||||
except smtplib.SMTPConnectError:
|
||||
self._logSys.error("Error connecting to host '%s'", self.host)
|
||||
raise
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
self._logSys.error(
|
||||
"Failed to authenticate with host '%s' user '%s'",
|
||||
self.host, self.user)
|
||||
raise
|
||||
except smtplib.SMTPException:
|
||||
self._logSys.error(
|
||||
"Error sending mail to host '%s' from '%s' to '%s'",
|
||||
self.host, self.fromaddr, self.toaddr)
|
||||
raise
|
||||
else:
|
||||
if failed_recipients:
|
||||
self._logSys.warning(
|
||||
"Email to '%s' failed to following recipients: %r",
|
||||
self.toaddr, failed_recipients)
|
||||
self._logSys.debug("Email '%s' successfully sent", subject)
|
||||
finally:
|
||||
try:
|
||||
self._logSys.debug("Disconnected from '%s', response %i: %s",
|
||||
self.host, *smtp.quit())
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
pass # Not connected
|
||||
|
||||
def start(self):
|
||||
"""Sends email to recipients informing that the jail has started.
|
||||
"""
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: started on %(hostname)s" %
|
||||
self.message_values,
|
||||
messages['start'] % self.message_values)
|
||||
|
||||
def stop(self):
|
||||
"""Sends email to recipients informing that the jail has stopped.
|
||||
"""
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: stopped on %(hostname)s" %
|
||||
self.message_values,
|
||||
messages['stop'] % self.message_values)
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Sends email to recipients informing that ban has occurred.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
aInfo.update(self.message_values)
|
||||
message = "".join([
|
||||
messages['ban']['head'],
|
||||
messages['ban'].get(self.matches, ""),
|
||||
messages['ban']['tail']
|
||||
])
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" %
|
||||
aInfo,
|
||||
message % aInfo)
|
||||
|
||||
Action = SMTPAction
|
|
@ -0,0 +1,124 @@
|
|||
# Fail2Ban action for sending xarf Login-Attack messages to IP owner
|
||||
#
|
||||
# IMPORTANT:
|
||||
#
|
||||
# Emailing a IP owner of abuse is a serious complain. 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
|
||||
#
|
||||
# DEPENDANCIES:
|
||||
#
|
||||
# This requires the dig command from bind-utils
|
||||
#
|
||||
# This uses the https://abusix.com/contactdb.html to lookup abuse contacts.
|
||||
#
|
||||
# XARF is a specification for sending a formatted response
|
||||
# for non-messaging based abuse including:
|
||||
#
|
||||
# Login-Attack, Malware-Attack, Fraud (Phishing, etc.), Info DNSBL
|
||||
#
|
||||
# For details see:
|
||||
# https://github.com/abusix/xarf-specification
|
||||
# http://www.x-arf.org/schemata.html
|
||||
#
|
||||
# Author: Daniel Black
|
||||
# Based on complain written by Russell Odom <russ@gloomytrousers.co.uk>
|
||||
#
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
actionstart =
|
||||
|
||||
actionstop =
|
||||
|
||||
actioncheck =
|
||||
|
||||
actionban = oifs=${IFS}; IFS=.;SEP_IP=( <ip> ); set -- ${SEP_IP}; ADDRESSES=$(dig +short -t txt -q $4.$3.$2.$1.abuse-contacts.abusix.org); IFS=${oifs}
|
||||
IP=<ip>
|
||||
FROM=<sender>
|
||||
SERVICE=<service>
|
||||
FAILURES=<failures>
|
||||
REPORTID=<time>@`uname -n`
|
||||
TLP=<tlp>
|
||||
PORT=<port>
|
||||
DATE=`LC_TIME=C date -u --date=@<time> +"%%a, %%d %%h %%Y %%T +0000"`
|
||||
if [ ! -z "$ADDRESSES" ]; then
|
||||
(printf -- %%b "<header>\n<message>\n<report>\n";
|
||||
date '+Note: Local timezone is %%z (%%Z)';
|
||||
printf -- %%b "<ipmatches>\n\n<footer>") | <mailcmd> <mailargs> ${ADDRESSES//,/\" \"}
|
||||
fi
|
||||
|
||||
actionunban =
|
||||
|
||||
[Init]
|
||||
# Option: header
|
||||
# Notes: This is really a fixed value
|
||||
header = Subject: abuse report about $IP - $DATE\nAuto-Submitted: auto-generated\nX-XARF: PLAIN\nContent-Transfer-Encoding: 7bit\nContent-Type: multipart/mixed; charset=utf8;\n boundary=Abuse-bfbb0f920793ac03cb8634bde14d8a1e;\n\n--Abuse-bfbb0f920793ac03cb8634bde14d8a1e\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf-8;\n
|
||||
|
||||
# Option: footer
|
||||
# Notes: This is really a fixed value and needs to match the report and header
|
||||
# mime delimiters
|
||||
footer = \n\n--Abuse-bfbb0f920793ac03cb8634bde14d8a1e--
|
||||
|
||||
# Option: report
|
||||
# Notes: Intended to be fixed
|
||||
report = --Abuse-bfbb0f920793ac03cb8634bde14d8a1e\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf-8; name=\"report.txt\";\n\n---\nReported-From: $FROM\nCategory: abuse\nReport-ID: $REPORTID\nReport-Type: login-attack\nService: $SERVICE\nVersion: 0.2\nUser-Agent: Fail2ban v0.9\nDate: $DATE\nSource-Type: ip-address\nSource: $IP\nPort: $PORT\nSchema-URL: http://www.x-arf.org/schema/abuse_login-attack_0.1.2.json\nAttachment: text/plain\nOccurances: $FAILURES\nTLP: $TLP\n\n\n--Abuse-bfbb0f920793ac03cb8634bde14d8a1e\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf8; name=\"logfile.log\";
|
||||
|
||||
# Option: Message
|
||||
# Notes: This can be modified by the users
|
||||
message = Dear Sir/Madam,\n\nWe have detected abuse from the IP address $IP, which according to abusix.com is on your network. We would appreciate if you would investigate and take action as appropriate.\n\nLog lines are given below, but please ask if you require any further information.\n\n(If you are not the correct person to contact about this please accept our apologies - your e-mail address was extracted from the whois record by an automated process.)\n\n This mail was generated by Fail2Ban in a X-ARF format! You can find more information about x-arf at http://www.x-arf.org/specification.html.\n\nThe recipient address of this report was provided by the Abuse Contact DB by abusix.com. abusix.com does not maintain the content of the database. All information which we pass out, derives from the RIR databases and is processed for ease of use. If you want to change or report non working abuse contacts please contact the appropriate RIR. If you have any further question, contact abusix.com directly via email (info@abusix.com). Information about the Abuse Contact Database can be found here: https://abusix.com/global-reporting/abuse-contact-db\nabusix.com is neither responsible nor liable for the content or accuracy of this message.\n
|
||||
|
||||
# Option: loglines
|
||||
# Notes.: The number of log lines to search for the IP for the report
|
||||
loglines = 9000
|
||||
|
||||
# Option: mailcmd
|
||||
# Notes.: Your system mail command. It is passed the recipient
|
||||
# Values: CMD
|
||||
#
|
||||
mailcmd = /usr/sbin/sendmail
|
||||
|
||||
# Option: mailargs
|
||||
# Notes.: Additional arguments to mail command. e.g. for standard Unix mail:
|
||||
# CC reports to another address:
|
||||
# -c me@example.com
|
||||
# Appear to come from a different address - the '--' indicates
|
||||
# arguments to be passed to Sendmail:
|
||||
# -- -f me@example.com
|
||||
# Values: [ STRING ]
|
||||
#
|
||||
mailargs = -f <sender>
|
||||
|
||||
# Option: tlp
|
||||
# Notes.: Traffic light protocol defining the sharing of this information.
|
||||
# http://www.trusted-introducer.org/ISTLPv11.pdf
|
||||
# green is share to those involved in network security but it is not
|
||||
# to be released to the public.
|
||||
tlp = green
|
||||
|
||||
# ALL of the following parameters should be set so the report contains
|
||||
# meaningful information
|
||||
|
||||
# Option: service
|
||||
# Notes.: This is the service type that was attacked. e.g. ssh, pop3
|
||||
service = unspecified
|
||||
|
||||
# Option: logpath
|
||||
# Notes: Path to the log files which contain relevant lines for the abuser IP
|
||||
# Values: Filename(s) space separated and can contain wildcards (these are
|
||||
# greped for the IP so make sure these aren't too long
|
||||
logpath = /dev/null
|
||||
|
||||
# Option: sender
|
||||
# Notes.: This is the sender that is included in the XARF report
|
||||
sender = fail2ban@`uname -n`
|
||||
|
||||
# Option: port
|
||||
# Notes.: This is the port number that received the login-attack
|
||||
port = 0
|
||||
|
|
@ -6,20 +6,22 @@
|
|||
# file, but provide customizations in fail2ban.local file, e.g.:
|
||||
#
|
||||
# [Definition]
|
||||
# loglevel = 4
|
||||
# loglevel = DEBUG
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: loglevel
|
||||
# Notes.: Set the log level output.
|
||||
# 1 = ERROR
|
||||
# 2 = WARN
|
||||
# 3 = INFO
|
||||
# 4 = DEBUG
|
||||
# Values: [ NUM ] Default: 1
|
||||
# CRITICAL
|
||||
# ERROR
|
||||
# WARNING
|
||||
# NOTICE
|
||||
# INFO
|
||||
# DEBUG
|
||||
# Values: [ LEVEL ] Default: ERROR
|
||||
#
|
||||
loglevel = 3
|
||||
loglevel = INFO
|
||||
|
||||
# Option: logtarget
|
||||
# Notes.: Set the log target. This could be a file, SYSLOG, STDERR or STDOUT.
|
||||
|
@ -47,3 +49,15 @@ socket = /var/run/fail2ban/fail2ban.sock
|
|||
#
|
||||
pidfile = /var/run/fail2ban/fail2ban.pid
|
||||
|
||||
# Options: dbfile
|
||||
# Notes.: Set the file for the fail2ban persistent data to be stored.
|
||||
# A value of ":memory:" means database is only stored in memory
|
||||
# and data is lost once fail2ban is stops.
|
||||
# A value of "None" disables the database.
|
||||
# Values: [ None :memory: FILE ] Default: /var/lib/fail2ban/fail2ban.sqlite3
|
||||
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
|
||||
|
||||
# Options: dbpurgeage
|
||||
# Notes.: Sets age at which bans should be purged from the database
|
||||
# Values: [ SECONDS ] Default: 86400 (24hours)
|
||||
dbpurgeage = 86400
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# Fail2Ban filter to match web requests for selected URLs that don't exist
|
||||
#
|
||||
# This filter is aimed at blocking specific URLs that don't exist. This
|
||||
# could be a set of URLs places in a Disallow: directive in robots.txt or
|
||||
# just some web services that don't exist caused bots are searching for
|
||||
# exploitable content. This filter is designed to have a low false postitive
|
||||
# rate due.
|
||||
#
|
||||
# An alternative to this is the apache-noscript filter which blocks all
|
||||
# types of scripts that don't exist.
|
||||
#
|
||||
#
|
||||
# This is normally a predefined list of exploitable or valuable web services
|
||||
# that are hidden or aren't actually installed.
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
# overwrite with apache-common.local if _apache_error_client is incorrect.
|
||||
before = apache-common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^%(_apache_error_client)s ((AH001(28|30): )?File does not exist|(AH01264: )?script not found or unable to stat): <webroot><block>(, referer: \S+)?\s*$
|
||||
^%(_apache_error_client)s script '<webroot><block>' not found or unable to stat(, referer: \S+)?\s*$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
||||
[Init]
|
||||
|
||||
# Webroot represents the webroot on which all other files are based
|
||||
webroot = /var/www/
|
||||
# Block is the actual non-found directories to block
|
||||
block = (<webmail>|<phpmyadmin>|<wordpress>)[^,]*
|
||||
|
||||
# These are just convient definitions that assist the blocking of stuff that
|
||||
# isn't installed
|
||||
webmail = roundcube|(ext)?mail|horde|(v-?)?webmail
|
||||
|
||||
phpmyadmin = (typo3/|xampp/|admin/|)(pma|(php)?[Mm]y[Aa]dmin)
|
||||
|
||||
wordpress = wp-(login|signup)\.php
|
||||
|
||||
|
||||
# DEV Notes:
|
||||
#
|
||||
# Author: Daniel Black
|
|
@ -8,7 +8,7 @@ after = apache-common.local
|
|||
|
||||
[DEFAULT]
|
||||
|
||||
_apache_error_client = \[[^]]*\] \[(:?error|\S+:\S+)\]( \[pid \d+(:\S+ \d+)?\])? \[client <HOST>(:\d{1,5})?\]
|
||||
_apache_error_client = \[\] \[(:?error|\S+:\S+)\]( \[pid \d+(:\S+ \d+)?\])? \[client <HOST>(:\d{1,5})?\]
|
||||
|
||||
# Common prefix for [error] apache messages which also would include <HOST>
|
||||
# Depending on the version it could be
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
# Fail2Ban filter to block web requests for scripts (on non scripted websites)
|
||||
#
|
||||
# This matches many types of scripts that don't exist. This could generate a
|
||||
# lot of false positive matches in cases like wikis and forums where users
|
||||
# no affiliated with the website can insert links to missing files/scripts into
|
||||
# pages and cause non-malicious browsers of the site to trigger against this
|
||||
# filter.
|
||||
#
|
||||
# If you'd like to match specific URLs that don't exist see the
|
||||
# apache-botsearch filter.
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
@ -19,6 +27,6 @@ ignoreregex =
|
|||
#
|
||||
# https://wiki.apache.org/httpd/ListOfErrors for apache error IDs
|
||||
#
|
||||
# Second regex, script '/\S*(\.php|\.asp|\.exe|\.pl)\S*' not found or unable to stat\s*$ is Before http-2.2
|
||||
# Second regex, script '/\S*(\.php|\.asp|\.exe|\.pl)\S*' not found or unable to stat\s*$ is in httpd-2.2
|
||||
#
|
||||
# Author: Cyril Jaquier
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Fail2Ban filter for failure attempts in Counter Strike-1.6
|
||||
#
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^: Bad Rcon: "rcon \d+ "\S+" sv_contact ".*?"" from "<HOST>:\d+"$
|
||||
|
||||
|
||||
[Init]
|
||||
|
||||
datepattern = ^L %%d/%%m/%%Y - %%H:%%M:%%S
|
||||
|
||||
|
||||
# Author: Daniel Black
|
||||
|
|
@ -15,6 +15,10 @@ failregex = ^%(__prefix_line)s(pam_unix(\(dovecot:auth\))?:)?\s+authentication f
|
|||
|
||||
ignoreregex =
|
||||
|
||||
[Init]
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=dovecot.service
|
||||
|
||||
# DEV Notes:
|
||||
# * the first regex is essentially a copy of pam-generic.conf
|
||||
# * Probably doesn't do dovecot sql/ldap backends properly
|
||||
|
|
|
@ -16,4 +16,22 @@
|
|||
# searched for other failures. This tag can be used multiple times.
|
||||
# Values: TEXT
|
||||
#
|
||||
failregex = ^(?:\.\d+)? \[info\] <0\.\d+\.\d>@ejabberd_c2s:wait_for_feature_request:\d+ \([^\)]+\) Failed authentication for \S+ from IP <HOST>$
|
||||
failregex = ^=INFO REPORT==== ===\nI\(<0\.\d+\.0>:ejabberd_c2s:\d+\) : \([^)]+\) Failed authentication for .+ from IP <HOST> \({{(?:\d+,){3}\d+},\d+}\)$
|
||||
^(?:\.\d+)? \[info\] <0\.\d+\.\d>@ejabberd_c2s:wait_for_feature_request:\d+ \([^\)]+\) Failed authentication for \S+ from IP <HOST>$
|
||||
|
||||
# Option: ignoreregex
|
||||
# Notes.: regex to ignore. If this regex matches, the line is ignored.
|
||||
# Values: TEXT
|
||||
#
|
||||
ignoreregex =
|
||||
|
||||
[Init]
|
||||
|
||||
# "maxlines" is number of log lines to buffer for multi-line regex searches
|
||||
maxlines = 2
|
||||
|
||||
# Option: journalmatch
|
||||
# Notes.: systemd journalctl style match filter for journal based backend
|
||||
# Values: TEXT
|
||||
#
|
||||
journalmatch =
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
# Fail2Ban filter for exim the spam rejection messages
|
||||
#
|
||||
## For the SA: Action: silently tossed message... to be logged exim's SAdevnull option needs to be used.
|
||||
# Honeypot traps are very useful for fighting spam. You just activate an email
|
||||
# address on your domain that you do not intend to use at all, and that normal
|
||||
# people do not risk to try for contacting you. It may be something that
|
||||
# spammers often test. You can also hide the address on a web page to be picked
|
||||
# by spam spiders. Or simply parse your mail logs for an invalid address
|
||||
# already being frequently targeted by spammers. Enable the address and
|
||||
# redirect it to the blackhole. In Exim's alias file, you would add the
|
||||
# following line (assuming the address is honeypot@yourdomain.com):
|
||||
#
|
||||
# honeypot: :blackhole:
|
||||
#
|
||||
# For the SA: Action: silently tossed message... to be logged exim's SAdevnull option needs to be used.
|
||||
#
|
||||
# To this filter use the jail.local should contain in the right jail:
|
||||
#
|
||||
# filter = exim-spam[honeypot=honeypot@yourdomain.com]
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
|
@ -13,10 +29,20 @@ before = exim-common.conf
|
|||
failregex = ^%(pid)s \S+ F=(<>|\S+@\S+) %(host_info)srejected by local_scan\(\): .{0,256}$
|
||||
^%(pid)s %(host_info)sF=(<>|[^@]+@\S+) rejected RCPT [^@]+@\S+: .*dnsbl.*\s*$
|
||||
^%(pid)s \S+ %(host_info)sF=(<>|[^@]+@\S+) rejected after DATA: This message contains a virus \(\S+\)\.\s*$
|
||||
^%(pid)s \S+ SA: Action: flagged as Spam but accepted: score=\d+\.\d+ required=\d+\.\d+ \(scanned in \d+/\d+ secs \| Message-Id: \S+\)\. From \S+ \(host=\S+ \[<HOST>\]\) for <honeypot>$
|
||||
^%(pid)s \S+ SA: Action: silently tossed message: score=\d+\.\d+ required=\d+\.\d+ trigger=\d+\.\d+ \(scanned in \d+/\d+ secs \| Message-Id: \S+\)\. From \S+ \(host=(\S+ )?\[<HOST>\]\) for \S+$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
[Init]
|
||||
|
||||
# Option: honeypot
|
||||
# Notes.: honeypot is an email address that isn't published anywhere that a
|
||||
# legitimate email sender would send email too.
|
||||
# Values: email address
|
||||
|
||||
honeypot = trap@example.com
|
||||
|
||||
# DEV Notes:
|
||||
# The %(host_info) defination contains a <HOST> match
|
||||
#
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Fail2Ban configuration file for guacamole
|
||||
#
|
||||
# Author: Steven Hiscocks
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: failregex
|
||||
# Notes.: regex to match the password failures messages in the logfile.
|
||||
# Values: TEXT
|
||||
#
|
||||
failregex = ^.*\nWARNING: Authentication attempt from <HOST> for user "[^"]*" failed\.$
|
||||
|
||||
# Option: ignoreregex
|
||||
# Notes.: regex to ignore. If this regex matches, the line is ignored.
|
||||
# Values: TEXT
|
||||
#
|
||||
ignoreregex =
|
||||
|
||||
[Init]
|
||||
# "maxlines" is number of log lines to buffer for multi-line regex searches
|
||||
maxlines = 2
|
|
@ -0,0 +1,17 @@
|
|||
# Fail2ban filter for kerio
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^ SMTP Spam attack detected from <HOST>,
|
||||
^ IP address <HOST> found in DNS blacklist \S+, mail from \S+ to \S+$
|
||||
^ Relay attempt from IP address <HOST>
|
||||
^ Attempt to deliver to unknown recipient \S+, from \S+, IP address <HOST>$
|
||||
[Init]
|
||||
|
||||
datepattern = ^\[%%d/%%b/%%Y %%H:%%M:%%S\]
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Author: A.P. Lawrence
|
||||
#
|
||||
# Based off: http://aplawrence.com/Kerio/fail2ban.html
|
|
@ -34,15 +34,13 @@ __daemon_combs_re=(?:%(__pid_re)s?:\s+%(__daemon_re)s|%(__daemon_re)s%(__pid_re)
|
|||
# this can be optional (for instance if we match named native log files)
|
||||
__line_prefix=(?:\s\S+ %(__daemon_combs_re)s\s+)?
|
||||
|
||||
failregex = ^%(__line_prefix)s(\.\d+)?( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: (view (internal|external): )?query(?: \(cache\))? '.*' denied\s*$
|
||||
^%(__line_prefix)s(\.\d+)?( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: zone transfer '\S+/AXFR/\w+' denied\s*$
|
||||
^%(__line_prefix)s(\.\d+)?( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: bad zone transfer request: '\S+/IN': non-authoritative zone \(NOTAUTH\)\s*$
|
||||
failregex = ^%(__line_prefix)s( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: (view (internal|external): )?query(?: \(cache\))? '.*' denied\s*$
|
||||
^%(__line_prefix)s( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: zone transfer '\S+/AXFR/\w+' denied\s*$
|
||||
^%(__line_prefix)s( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: bad zone transfer request: '\S+/IN': non-authoritative zone \(NOTAUTH\)\s*$
|
||||
|
||||
# DEV Notes:
|
||||
# Trying to generalize the
|
||||
# structure which is general to capture general patterns in log
|
||||
# lines to cover different configurations/distributions
|
||||
#
|
||||
# (\.\d+)? is a really ugly catch of the microseconds not captured in the date detector
|
||||
#
|
||||
# Author: Yaroslav Halchenko
|
||||
|
|
|
@ -19,4 +19,8 @@ failregex = ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 554 5\.7
|
|||
|
||||
ignoreregex =
|
||||
|
||||
[Init]
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=postfix.service
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
|
|
|
@ -21,12 +21,16 @@ before = common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
_daemon = fail2ban\.actions
|
||||
_daemon = fail2ban\.server\.actions
|
||||
|
||||
# The name of the jail that this filter is used for. In jail.conf, name the
|
||||
# jail using this filter 'recidive', or change this line!
|
||||
_jailname = recidive
|
||||
|
||||
failregex = ^(%(__prefix_line)s|,\d{3} fail2ban.actions%(__pid_re)s?:\s+)WARNING\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$
|
||||
failregex = ^(%(__prefix_line)s| %(_daemon)s%(__pid_re)s?:\s+)WARNING\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$
|
||||
|
||||
[Init]
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=fail2ban.service PRIORITY=4
|
||||
|
||||
# Author: Tom Hendrikx, modifications by Amir Caspi
|
||||
|
|
|
@ -9,7 +9,7 @@ before = common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
failregex = ^\s*(\[(\s[+-][0-9]{4})?\])?(%(__hostname)s roundcube: IMAP Error)?: (FAILED login|Login failed) for .*? from <HOST>(\. .* in .*?/rcube_imap\.php on line \d+ \(\S+ \S+\))?$
|
||||
failregex = ^\s*(\[\])?(%(__hostname)s roundcube: IMAP Error)?: (FAILED login|Login failed) for .*? from <HOST>(\. .* in .*?/rcube_imap\.php on line \d+ \(\S+ \S+\))?$
|
||||
|
||||
ignoreregex =
|
||||
# DEV Notes:
|
||||
|
|
|
@ -19,16 +19,32 @@ before = common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
_daemon = (?:sm-(mta|acceptingconnections))
|
||||
_daemon = (?:(sm-(mta|acceptingconnections)|sendmail))
|
||||
|
||||
failregex = ^%(__prefix_line)s\w{14}: ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[<HOST>\]( \(may be forged\))?, reject=(550 5\.7\.1 (?P=email)\.\.\. Relaying denied\. (IP name possibly forged \[(\d+\.){3}\d+\]|Proper authentication required\.|IP name lookup failed \[(\d+\.){3}\d+\])|553 5\.1\.8 (?P=email)\.\.\. Domain of sender address \S+ does not exist|550 5\.[71]\.1 (?P=email)\.\.\. (Rejected: .*|User unknown))$
|
||||
^%(__prefix_line)sruleset=check_relay, arg1=(?P<dom>\S+), arg2=<HOST>, relay=((?P=dom) )?\[(\d+\.){3}\d+\]( \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$
|
||||
^%(__prefix_line)s\w{14}: rejecting commands from (\S+ )?\[<HOST>\] due to pre-greeting traffic after \d+ seconds$
|
||||
^%(__prefix_line)s\w{14}: (\S+ )?\[<HOST>\]: ((?i)expn|vrfy) \S+ \[rejected\]$
|
||||
^(?P<__prefix>%(__prefix_line)s\w+: )<[^@]+@[^>]+>\.\.\. No such user here<SKIPLINES>(?P=__prefix)from=<[^@]+@[^>]+>, size=\d+, class=\d+, nrcpts=\d+, bodytype=\w+, proto=E?SMTP, daemon=MTA, relay=\S+ \[<HOST>\]$
|
||||
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# DEV Notes:
|
||||
|
||||
[Init]
|
||||
|
||||
# "maxlines" is number of log lines to buffer for multi-line regex searches
|
||||
maxlines = 10
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Regarding the last multiline regex:
|
||||
#
|
||||
# There can be a nunber of non-related lines between the first and second part
|
||||
# of this regex maxlines of 10 is quite generious. Only one of the
|
||||
# "No such user" lines needs to be matched before the line with the HOST.
|
||||
#
|
||||
# Note the capture __prefix, includes both the __prefix_lines (which includes
|
||||
# the sendmail PID), but also the \w+ which the the sendmail assigned mail ID.
|
||||
#
|
||||
# Author: Daniel Black and Fabian Wenk
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
[Definition]
|
||||
|
||||
failregex = ^ \[LOGIN_ERROR\].*from <HOST>: Unknown user or password incorrect\.$
|
||||
|
||||
|
||||
[Init]
|
||||
|
||||
datepattern = ^%%m/%%d/%%Y %%H:%%M:%%S
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Author: Daniel Black
|
|
@ -22,4 +22,8 @@ failregex = ^%(__prefix_line)sDid not receive identification string from <HOST>\
|
|||
|
||||
ignoreregex =
|
||||
|
||||
[Init]
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
|
||||
|
||||
# Author: Yaroslav Halchenko
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
# Fail2Ban filter for openssh
|
||||
#
|
||||
# If you want to protect OpenSSH from being bruteforced by password
|
||||
# authentication then get public key authentication working before disabling
|
||||
# PasswordAuthentication in sshd_config.
|
||||
#
|
||||
#
|
||||
# "Connection from <HOST> port \d+" requires LogLevel VERBOSE in sshd_config
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
|
@ -7,7 +14,6 @@
|
|||
# common.local
|
||||
before = common.conf
|
||||
|
||||
|
||||
[Definition]
|
||||
|
||||
_daemon = sshd
|
||||
|
@ -24,9 +30,19 @@ failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|erro
|
|||
^%(__prefix_line)sReceived disconnect from <HOST>: 3: \S+: Auth fail$
|
||||
^%(__prefix_line)sUser .+ from <HOST> not allowed because a group is listed in DenyGroups\s*$
|
||||
^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups\s*$
|
||||
^(?P<__prefix>%(__prefix_line)s)User .+ not allowed because account is locked<SKIPLINES>(?P=__prefix)(?:error: )?Received disconnect from <HOST>: 11: Bye Bye \[preauth\]$
|
||||
^(?P<__prefix>%(__prefix_line)s)Disconnecting: Too many authentication failures for .+? \[preauth\]<SKIPLINES>(?P=__prefix)(?:error: )?Connection closed by <HOST> \[preauth\]$
|
||||
^(?P<__prefix>%(__prefix_line)s)Connection from <HOST> port \d+<SKIPLINES>(?P=__prefix)Disconnecting: Too many authentication failures for .+? \[preauth\]$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
[Init]
|
||||
|
||||
# "maxlines" is number of log lines to buffer for multi-line regex searches
|
||||
maxlines = 10
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
|
||||
|
||||
# DEV Notes:
|
||||
#
|
||||
# "Failed \S+ for .*? from <HOST>..." failregex uses non-greedy catch-all because
|
||||
|
@ -35,3 +51,4 @@ ignoreregex =
|
|||
# matched away first.
|
||||
#
|
||||
# Author: Cyril Jaquier, Yaroslav Halchenko, Petr Voralek, Daniel Black
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Fail2ban filter for stunnel
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^ LOG\d\[\d+:\d+\]:\ SSL_accept from <HOST>:\d+ : (?P<CODE>[\dA-F]+): error:(?P=CODE):SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not return a certificate$
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Author: Daniel Black
|
||||
#
|
||||
# Based off: http://www.fail2ban.org/wiki/index.php/Fail2ban:Community_Portal#stunnel4
|
|
@ -0,0 +1,21 @@
|
|||
# Fail2Ban filter for Tine 2.0 authentication
|
||||
#
|
||||
# Enable logging with:
|
||||
# $config['info_log']='/var/log/tine20/tine20.log';
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^[\da-f]{5,} [\da-f]{5,} (-- none --|.*?)( \d+(\.\d+)?(h|m|s|ms)){0,2} - WARN \(\d+\): Tinebase_Controller::login::\d+ Login with username .*? from <HOST> failed \(-[13]\)!$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Author: Mika (mkl) from Tine20.org forum: https://www.tine20.org/forum/viewtopic.php?f=2&t=15688&p=54766
|
||||
# Editor: Daniel Black
|
||||
# Advisor: Lars Kneschke
|
||||
#
|
||||
# Usernames can contain spaces.
|
||||
#
|
||||
# Authentication: http://git.tine20.org/git?p=tine20;a=blob;f=tine20/Tinebase/Controller.php#l105
|
||||
# Logger: http://git.tine20.org/git?p=tine20;a=blob;f=tine20/Tinebase/Log/Formatter.php
|
||||
# formatMicrotimeDiff: http://git.tine20.org/git?p=tine20;a=blob;f=tine20/Tinebase/Helper.php#l276
|
1085
config/jail.conf
1085
config/jail.conf
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,57 @@
|
|||
# Common
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
after = paths-overrides.local
|
||||
|
||||
[DEFAULT]
|
||||
|
||||
|
||||
|
||||
sshd_log = %(syslog_authpriv)s
|
||||
|
||||
dropbear_log = %(syslog_authpriv)s
|
||||
|
||||
|
||||
# from /etc/audit/auditd.conf
|
||||
auditd_log = /var/log/audit/audit.log
|
||||
|
||||
|
||||
nginx_error_log = /var/log/nginx/error.log
|
||||
|
||||
nginx_access_log = /var/log/nginx/access.log
|
||||
|
||||
|
||||
lighttpd_error_log = /var/log/lighttpd/error.log
|
||||
|
||||
# http://www.hardened-php.net/suhosin/configuration.html#suhosin.log.syslog.facility
|
||||
# syslog_user is the default. Lighttpd also hooks errors into its log.
|
||||
|
||||
suhosin_log = %(syslog_user)s %(lighttpd_error_log)s
|
||||
|
||||
# defaults to ftp or local2 if ftp doesn't exist
|
||||
proftpd_log = %(syslog_ftp)s
|
||||
|
||||
# http://svnweb.freebsd.org/ports/head/ftp/proftpd/files/patch-src_proftpd.8.in?view=markup
|
||||
# defaults to ftp but can be overwritten.
|
||||
pureftpd_log = %(syslog_ftp)s
|
||||
|
||||
# ftp, daemon and then local7 are tried at configure time however it is overwriteable at configure time
|
||||
#
|
||||
wuftpd_log = %(syslog_ftp)s
|
||||
|
||||
# syslog_enable defaults to no. so it defaults to vsftpd_log_file setting of /var/log/vsftpd.log
|
||||
# No distro seems to set it to syslog by default
|
||||
# If syslog set it defaults to ftp facility if exists at compile time otherwise falls back to daemonlog.
|
||||
vsftpd_log = /var/log/vsftpd.log
|
||||
|
||||
# Technically syslog_facility in main.cf can overwrite but no-one sane does this.
|
||||
postfix_log = %(syslog_mail_warn)s
|
||||
|
||||
dovecot_log = %(syslog_mail_warn)s
|
||||
|
||||
# Seems to be set at compile time only to LOG_LOCAL0 (src/const.h) at Notice level
|
||||
solidpop3d_log = %(syslog_local0)s
|
||||
|
||||
mysql_log = %(syslog_daemon)s
|
|
@ -0,0 +1,39 @@
|
|||
# Debian
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = paths-common.conf
|
||||
|
||||
after = paths-overrides.local
|
||||
|
||||
|
||||
[DEFAULT]
|
||||
|
||||
syslog_mail = /var/log/mail.log
|
||||
|
||||
syslog_mail_warn = /var/log/mail.warn
|
||||
|
||||
syslog_authpriv = /var/log/auth.log
|
||||
|
||||
# syslog_auth = /var/log/auth.log
|
||||
#
|
||||
syslog_user = /var/log/user.log
|
||||
|
||||
syslog_ftp = /var/log/syslog
|
||||
|
||||
syslog_daemon = /var/log/daemon.log
|
||||
|
||||
syslog_local0 = /var/log/messages
|
||||
|
||||
|
||||
apache_error_log = /var/log/apache2/*error.log
|
||||
|
||||
apache_access_log = /var/log/apache2/*access.log
|
||||
|
||||
|
||||
# was in debian squeezy but not in wheezy
|
||||
# /etc/proftpd/proftpd.conf (SystemLog)
|
||||
proftpd_log = /var/log/proftpd/proftpd.log
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# Fedora
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = paths-common.conf
|
||||
|
||||
after = paths-overrides.local
|
||||
|
||||
|
||||
[DEFAULT]
|
||||
|
||||
syslog_mail = /var/log/maillog
|
||||
|
||||
syslog_mail_warn = /var/log/maillog
|
||||
|
||||
syslog_authpriv = /var/log/secure
|
||||
|
||||
syslog_user = /var/log/messages
|
||||
|
||||
syslog_ftp = /var/log/messages
|
||||
|
||||
syslog_daemon = /var/log/messages
|
||||
|
||||
syslog_local0 = /var/log/messages
|
||||
|
||||
|
||||
apache_error_log = /var/log/httpd/*error_log
|
||||
|
||||
apache_access_log = /var/log/httpd/*access_log
|
||||
|
||||
# /etc/proftpd/proftpd.conf (ExtendedLog for Anonymous)
|
||||
# proftpd_log = /var/log/proftpd/auth.log
|
||||
# Tested and it worked out in /var/log/messages so assuming syslog_ftp for now.
|
||||
|
||||
mysql_log = /var/lib/mysql/mysqld.log
|
|
@ -0,0 +1,46 @@
|
|||
# FreeBSD
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = common-paths.conf
|
||||
|
||||
after = paths-overrides.local
|
||||
|
||||
|
||||
[DEFAULT]
|
||||
|
||||
# http://www.freebsd.org/doc/handbook/configtuning-syslog.html
|
||||
#
|
||||
syslog_mail = /var/log/maillog
|
||||
|
||||
syslog_mail_warn = /var/log/maillog
|
||||
|
||||
syslog_authpriv = /var/log/auth.log
|
||||
|
||||
# note - is only ftp.info - if notice /var/log/messages may be needed
|
||||
syslog_ftp = /var/log/xferlog
|
||||
|
||||
syslog_daemon = /var/log/messages
|
||||
|
||||
syslog_local0 = /var/log/messages
|
||||
|
||||
# Linux things
|
||||
# we fake to avoid parse error in startups
|
||||
|
||||
auditd_log = /dev/null
|
||||
|
||||
# http://svnweb.freebsd.org/ports/head/www/apache24/files/patch-docs__conf__extra__httpd-ssl.conf.in?view=markup
|
||||
# http://svnweb.freebsd.org/ports/head/www/apache22/files/patch-docs__conf__extra__httpd-ssl.conf.in?view=markup
|
||||
# http://svnweb.freebsd.org/ports/head/www/apache24/files/patch-config.layout
|
||||
# http://svnweb.freebsd.org/ports/head/www/apache22/files/patch-config.layout
|
||||
|
||||
apache_error_log = /usr/local/www/logs/*error[_.]log
|
||||
|
||||
apache_access_log = /usr/local/www/logs/*access[_.]log
|
||||
|
||||
# http://svnweb.freebsd.org/ports/head/www/nginx/Makefile?view=markup
|
||||
|
||||
nginx_error_log = /var/log/nginx-error.log
|
||||
|
||||
nginx_access_log = /var/log/nginx-access.log
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# OSX
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = common-paths.conf
|
||||
|
||||
after = paths-overrides.local
|
||||
|
||||
|
||||
[DEFAULT]
|
||||
|
||||
syslog_mail = /var/log/mail.log
|
||||
|
||||
syslog_mail_warn = /var/log/mail.warn
|
||||
|
||||
syslog_authpriv = /var/log/secure.log
|
||||
#syslog_auth =
|
||||
|
||||
#syslog_user =
|
||||
|
||||
#syslog_ftp =
|
||||
|
||||
#syslog_daemon =
|
||||
|
||||
#syslog_local0 =
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
# This script carries out conversion of fail2ban to python3
|
||||
# A backup of any converted files are created with ".bak"
|
||||
# extension
|
||||
|
||||
set -eu
|
||||
|
||||
if 2to3 -w --no-diffs bin/* fail2ban;then
|
||||
echo "Success!" >&2
|
||||
exit 0
|
||||
else
|
||||
echo "Fail!" >&2
|
||||
exit 1
|
||||
fi
|
|
@ -1,251 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
"""Script to run Fail2Ban tests battery
|
||||
"""
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012- Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
|
||||
import unittest, logging, sys, time, os
|
||||
|
||||
if sys.version_info >= (2, 6):
|
||||
import json
|
||||
else: # pragma: no cover
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
json = None
|
||||
|
||||
from common.version import version
|
||||
from testcases import banmanagertestcase
|
||||
from testcases import clientreadertestcase
|
||||
from testcases import failmanagertestcase
|
||||
from testcases import filtertestcase
|
||||
from testcases import servertestcase
|
||||
from testcases import datedetectortestcase
|
||||
from testcases import actiontestcase
|
||||
from testcases import sockettestcase
|
||||
from testcases import misctestcase
|
||||
if json:
|
||||
from testcases import samplestestcase
|
||||
|
||||
from testcases.utils import FormatterWithTraceBack
|
||||
from testcases import actionstestcase
|
||||
from server.mytime import MyTime
|
||||
|
||||
from optparse import OptionParser, Option
|
||||
|
||||
def get_opt_parser():
|
||||
# use module docstring for help output
|
||||
p = OptionParser(
|
||||
usage="%s [OPTIONS] [regexps]\n" % sys.argv[0] + __doc__,
|
||||
version="%prog " + version)
|
||||
|
||||
p.add_options([
|
||||
Option('-l', "--log-level", type="choice",
|
||||
dest="log_level",
|
||||
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",
|
||||
dest="no_network",
|
||||
help="Do not run tests that require the network"),
|
||||
Option("-t", "--log-traceback", action='store_true',
|
||||
help="Enrich log-messages with compressed tracebacks"),
|
||||
Option("--full-traceback", action='store_true',
|
||||
help="Either to make the tracebacks full, not compressed (as by default)"),
|
||||
|
||||
])
|
||||
|
||||
return p
|
||||
|
||||
parser = get_opt_parser()
|
||||
(opts, regexps) = parser.parse_args()
|
||||
|
||||
#
|
||||
# Logging
|
||||
#
|
||||
logSys = logging.getLogger("fail2ban")
|
||||
|
||||
# Numerical level of verbosity corresponding to a log "level"
|
||||
verbosity = {'heavydebug': 4,
|
||||
'debug': 3,
|
||||
'info': 2,
|
||||
'warning': 1,
|
||||
'error': 1,
|
||||
'fatal': 0,
|
||||
None: 1}[opts.log_level]
|
||||
|
||||
if opts.log_level is not None: # pragma: no cover
|
||||
# so we had explicit settings
|
||||
logSys.setLevel(getattr(logging, opts.log_level.upper()))
|
||||
else: # pragma: no cover
|
||||
# suppress the logging but it would leave unittests' progress dots
|
||||
# ticking, unless like with '-l fatal' which would be silent
|
||||
# unless error occurs
|
||||
logSys.setLevel(getattr(logging, 'FATAL'))
|
||||
|
||||
# Add the default logging handler
|
||||
stdout = logging.StreamHandler(sys.stdout)
|
||||
|
||||
fmt = ' %(message)s'
|
||||
|
||||
if opts.log_traceback or opts.full_traceback:
|
||||
Formatter = FormatterWithTraceBack
|
||||
fmt = (opts.full_traceback and ' %(tb)s' or ' %(tbc)s') + fmt
|
||||
else:
|
||||
Formatter = logging.Formatter
|
||||
|
||||
# Custom log format for the verbose tests runs
|
||||
if verbosity > 1: # pragma: no cover
|
||||
stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt))
|
||||
else: # pragma: no cover
|
||||
# just prefix with the space
|
||||
stdout.setFormatter(Formatter(fmt))
|
||||
logSys.addHandler(stdout)
|
||||
|
||||
#
|
||||
# Let know the version
|
||||
#
|
||||
if not opts.log_level or opts.log_level != 'fatal': # pragma: no cover
|
||||
print "Fail2ban %s test suite. Python %s. Please wait..." \
|
||||
% (version, str(sys.version).replace('\n', ''))
|
||||
|
||||
|
||||
#
|
||||
# Gather the tests
|
||||
#
|
||||
if not len(regexps): # pragma: no cover
|
||||
tests = unittest.TestSuite()
|
||||
else: # pragma: no cover
|
||||
import re
|
||||
class FilteredTestSuite(unittest.TestSuite):
|
||||
_regexps = [re.compile(r) for r in regexps]
|
||||
def addTest(self, suite):
|
||||
suite_str = str(suite)
|
||||
for r in self._regexps:
|
||||
if r.search(suite_str):
|
||||
super(FilteredTestSuite, self).addTest(suite)
|
||||
return
|
||||
|
||||
tests = FilteredTestSuite()
|
||||
|
||||
# Server
|
||||
#tests.addTest(unittest.makeSuite(servertestcase.StartStop))
|
||||
tests.addTest(unittest.makeSuite(servertestcase.Transmitter))
|
||||
tests.addTest(unittest.makeSuite(servertestcase.JailTests))
|
||||
tests.addTest(unittest.makeSuite(servertestcase.RegexTests))
|
||||
tests.addTest(unittest.makeSuite(actiontestcase.ExecuteAction))
|
||||
tests.addTest(unittest.makeSuite(actionstestcase.ExecuteActions))
|
||||
# FailManager
|
||||
tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure))
|
||||
# BanManager
|
||||
tests.addTest(unittest.makeSuite(banmanagertestcase.AddFailure))
|
||||
# ClientReaders
|
||||
tests.addTest(unittest.makeSuite(clientreadertestcase.ConfigReaderTest))
|
||||
tests.addTest(unittest.makeSuite(clientreadertestcase.JailReaderTest))
|
||||
tests.addTest(unittest.makeSuite(clientreadertestcase.JailsReaderTest))
|
||||
# CSocket and AsyncServer
|
||||
tests.addTest(unittest.makeSuite(sockettestcase.Socket))
|
||||
# Misc helpers
|
||||
tests.addTest(unittest.makeSuite(misctestcase.HelpersTest))
|
||||
tests.addTest(unittest.makeSuite(misctestcase.SetupTest))
|
||||
tests.addTest(unittest.makeSuite(misctestcase.TestsUtilsTest))
|
||||
tests.addTest(unittest.makeSuite(misctestcase.CustomDateFormatsTest))
|
||||
|
||||
# Filter
|
||||
if not opts.no_network:
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIPDNS))
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.IgnoreIP))
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.BasicFilter))
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.LogFile))
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.LogFileFilterPoll))
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.LogFileMonitor))
|
||||
if not opts.no_network:
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.GetFailures))
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsTests))
|
||||
tests.addTest(unittest.makeSuite(filtertestcase.JailTests))
|
||||
|
||||
# DateDetector
|
||||
tests.addTest(unittest.makeSuite(datedetectortestcase.DateDetectorTest))
|
||||
if json:
|
||||
# Filter Regex tests with sample logs
|
||||
tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex))
|
||||
else: # pragma: no cover
|
||||
print "I: Skipping filter samples testing. No simplejson/json module"
|
||||
|
||||
#
|
||||
# Extensive use-tests of different available filters backends
|
||||
#
|
||||
|
||||
from server.filterpoll import FilterPoll
|
||||
filters = [FilterPoll] # always available
|
||||
|
||||
# Additional filters available only if external modules are available
|
||||
# yoh: Since I do not know better way for parametric tests
|
||||
# with good old unittest
|
||||
try:
|
||||
from server.filtergamin import FilterGamin
|
||||
filters.append(FilterGamin)
|
||||
except Exception, e: # pragma: no cover
|
||||
print "I: Skipping gamin backend testing. Got exception '%s'" % e
|
||||
|
||||
try:
|
||||
from server.filterpyinotify import FilterPyinotify
|
||||
filters.append(FilterPyinotify)
|
||||
except Exception, e: # pragma: no cover
|
||||
print "I: Skipping pyinotify backend testing. Got exception '%s'" % e
|
||||
|
||||
for Filter_ in filters:
|
||||
tests.addTest(unittest.makeSuite(
|
||||
filtertestcase.get_monitor_failures_testcase(Filter_)))
|
||||
|
||||
# Server test for logging elements which break logging used to support
|
||||
# testcases analysis
|
||||
tests.addTest(unittest.makeSuite(servertestcase.TransmitterLogging))
|
||||
|
||||
#
|
||||
# Run the tests
|
||||
#
|
||||
testRunner = unittest.TextTestRunner(verbosity=verbosity)
|
||||
|
||||
try:
|
||||
# Set the time to a fixed, known value
|
||||
# Sun Aug 14 12:00:00 CEST 2005
|
||||
# yoh: we need to adjust TZ to match the one used by Cyril so all the timestamps match
|
||||
old_TZ = os.environ.get('TZ', None)
|
||||
os.environ['TZ'] = 'Europe/Zurich'
|
||||
time.tzset()
|
||||
MyTime.setTime(1124013600)
|
||||
|
||||
tests_results = testRunner.run(tests)
|
||||
|
||||
finally: # pragma: no cover
|
||||
# Just for the sake of it reset the TZ
|
||||
# yoh: move all this into setup/teardown methods within tests
|
||||
os.environ.pop('TZ')
|
||||
if old_TZ:
|
||||
os.environ['TZ'] = old_TZ
|
||||
time.tzset()
|
||||
|
||||
if not tests_results.wasSuccessful(): # pragma: no cover
|
||||
sys.exit(1)
|
|
@ -9,7 +9,7 @@ for python in /usr/{,local/}bin/python2.[0-9]{,.*}{,-dbg}
|
|||
do
|
||||
[ -e "$python" ] || continue
|
||||
echo "Testing using $python"
|
||||
$python ./fail2ban-testcases "$@" || failed+=" $python"
|
||||
$python bin/fail2ban-testcases "$@" || failed+=" $python"
|
||||
done
|
||||
|
||||
if [ ! -z "$failed" ]; then
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
# Simple helper script to exercise unittests using all available
|
||||
# (under /usr/bin and /usr/local/bin python3.*)
|
||||
|
||||
set -eu
|
||||
|
||||
failed=
|
||||
for python in /usr/{,local/}bin/python3.[0-9]{,.*}{,-dbg}
|
||||
do
|
||||
[ -e "$python" ] || continue
|
||||
echo "Testing using $python"
|
||||
$python bin/fail2ban-testcases "$@" || failed+=" $python"
|
||||
done
|
||||
|
||||
if [ ! -z "$failed" ]; then
|
||||
echo "E: Failed with $failed"
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,70 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging.handlers
|
||||
|
||||
# Custom debug level
|
||||
logging.HEAVYDEBUG = 5
|
||||
|
||||
"""
|
||||
Below derived from:
|
||||
https://mail.python.org/pipermail/tutor/2007-August/056243.html
|
||||
"""
|
||||
|
||||
logging.NOTICE = logging.INFO + 5
|
||||
logging.addLevelName(logging.NOTICE, 'NOTICE')
|
||||
|
||||
# define a new logger function for notice
|
||||
# this is exactly like existing info, critical, debug...etc
|
||||
def _Logger_notice(self, msg, *args, **kwargs):
|
||||
"""
|
||||
Log 'msg % args' with severity 'NOTICE'.
|
||||
|
||||
To pass exception information, use the keyword argument exc_info with
|
||||
a true value, e.g.
|
||||
|
||||
logger.notice("Houston, we have a %s", "major disaster", exc_info=1)
|
||||
"""
|
||||
if self.isEnabledFor(logging.NOTICE):
|
||||
self._log(logging.NOTICE, msg, args, **kwargs)
|
||||
|
||||
logging.Logger.notice = _Logger_notice
|
||||
|
||||
# define a new root level notice function
|
||||
# this is exactly like existing info, critical, debug...etc
|
||||
def _root_notice(msg, *args, **kwargs):
|
||||
"""
|
||||
Log a message with severity 'NOTICE' on the root logger.
|
||||
"""
|
||||
if len(logging.root.handlers) == 0:
|
||||
logging.basicConfig()
|
||||
logging.root.notice(msg, *args, **kwargs)
|
||||
|
||||
# make the notice root level function known
|
||||
logging.notice = _root_notice
|
||||
|
||||
# add NOTICE to the priority map of all the levels
|
||||
logging.handlers.SysLogHandler.priority_map['NOTICE'] = 'notice'
|
|
@ -0,0 +1,78 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging, os
|
||||
|
||||
from .configreader import ConfigReader, DefinitionInitConfigReader
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class ActionReader(DefinitionInitConfigReader):
|
||||
|
||||
_configOpts = [
|
||||
["string", "actionstart", ""],
|
||||
["string", "actionstop", ""],
|
||||
["string", "actioncheck", ""],
|
||||
["string", "actionban", ""],
|
||||
["string", "actionunban", ""],
|
||||
]
|
||||
|
||||
def __init__(self, file_, jailName, initOpts, **kwargs):
|
||||
self._name = initOpts.get("actname", file_)
|
||||
DefinitionInitConfigReader.__init__(
|
||||
self, file_, jailName, initOpts, **kwargs)
|
||||
|
||||
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()
|
||||
stream.append(head + ["addaction", self._name])
|
||||
head.extend(["action", self._name])
|
||||
for opt in self._opts:
|
||||
if opt == "actionstart":
|
||||
stream.append(head + ["actionstart", self._opts[opt]])
|
||||
elif opt == "actionstop":
|
||||
stream.append(head + ["actionstop", self._opts[opt]])
|
||||
elif opt == "actioncheck":
|
||||
stream.append(head + ["actioncheck", self._opts[opt]])
|
||||
elif opt == "actionban":
|
||||
stream.append(head + ["actionban", self._opts[opt]])
|
||||
elif opt == "actionunban":
|
||||
stream.append(head + ["actionunban", self._opts[opt]])
|
||||
if self._initOpts:
|
||||
for p in self._initOpts:
|
||||
stream.append(head + [p, self._initOpts[p]])
|
||||
|
||||
return stream
|
|
@ -23,10 +23,10 @@ __license__ = "GPL"
|
|||
|
||||
import logging
|
||||
|
||||
from common.exceptions import UnknownJailException, DuplicateJailException
|
||||
from ..exceptions import UnknownJailException, DuplicateJailException
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
##
|
||||
# Beautify the output of the client.
|
||||
|
@ -67,28 +67,23 @@ class Beautifier:
|
|||
msg = "logs: " + response
|
||||
elif inC[0:1] == ['status']:
|
||||
if len(inC) > 1:
|
||||
# Create IP list
|
||||
ipList = ""
|
||||
for ip in response[1][1][2][1]:
|
||||
ipList += ip + " "
|
||||
# Creates file list.
|
||||
fileList = ""
|
||||
for f in response[0][1][2][1]:
|
||||
fileList += f + " "
|
||||
# Display information
|
||||
msg = "Status for the jail: " + inC[1] + "\n"
|
||||
msg = msg + "|- " + response[0][0] + "\n"
|
||||
msg = msg + "| |- " + response[0][1][2][0] + ":\t" + fileList + "\n"
|
||||
msg = msg + "| |- " + response[0][1][0][0] + ":\t" + `response[0][1][0][1]` + "\n"
|
||||
msg = msg + "| `- " + response[0][1][1][0] + ":\t" + `response[0][1][1][1]` + "\n"
|
||||
msg = msg + "`- " + response[1][0] + "\n"
|
||||
msg = msg + " |- " + response[1][1][0][0] + ":\t" + `response[1][1][0][1]` + "\n"
|
||||
msg = msg + " | `- " + response[1][1][2][0] + ":\t" + ipList + "\n"
|
||||
msg = msg + " `- " + response[1][1][1][0] + ":\t" + `response[1][1][1][1]`
|
||||
msg = ["Status for the jail: %s" % inC[1]]
|
||||
for n, res1 in enumerate(response):
|
||||
prefix1 = "`-" if n == len(response) - 1 else "|-"
|
||||
msg.append("%s %s" % (prefix1, res1[0]))
|
||||
prefix1 = " " if n == len(response) - 1 else "| "
|
||||
for m, res2 in enumerate(res1[1]):
|
||||
prefix2 = prefix1 + ("`-" if m == len(res1[1]) - 1 else "|-")
|
||||
val = " ".join(res2[1]) if isinstance(res2[1], list) else res2[1]
|
||||
msg.append("%s %s:\t%s" % (prefix2, res2[0], val))
|
||||
else:
|
||||
msg = "Status\n"
|
||||
msg = msg + "|- " + response[0][0] + ":\t" + `response[0][1]` + "\n"
|
||||
msg = msg + "`- " + response[1][0] + ":\t\t" + response[1][1]
|
||||
msg = ["Status"]
|
||||
for n, res1 in enumerate(response):
|
||||
prefix1 = "`-" if n == len(response) - 1 else "|-"
|
||||
val = " ".join(res1[1]) if isinstance(res1[1], list) else res1[1]
|
||||
msg.append("%s %s:\t%s" % (prefix1, res1[0], val))
|
||||
msg = "\n".join(msg)
|
||||
elif inC[1] == "logtarget":
|
||||
msg = "Current logging target is:\n"
|
||||
msg = msg + "`- " + response
|
||||
|
@ -104,6 +99,18 @@ class Beautifier:
|
|||
msg = msg + "DEBUG"
|
||||
else:
|
||||
msg = msg + `response`
|
||||
elif inC[1] == "dbfile":
|
||||
if response is None:
|
||||
msg = "Database currently disabled"
|
||||
else:
|
||||
msg = "Current database file is:\n"
|
||||
msg = msg + "`- " + response
|
||||
elif inC[1] == "dbpurgeage":
|
||||
if response is None:
|
||||
msg = "Database currently disabled"
|
||||
else:
|
||||
msg = "Current database purge age is:\n"
|
||||
msg = msg + "`- %iseconds" % response
|
||||
elif inC[2] in ("logpath", "addlogpath", "dellogpath"):
|
||||
if len(response) == 0:
|
||||
msg = "No file is currently monitored"
|
||||
|
@ -112,6 +119,23 @@ class Beautifier:
|
|||
for path in response[:-1]:
|
||||
msg = msg + "|- " + path + "\n"
|
||||
msg = msg + "`- " + response[len(response)-1]
|
||||
elif inC[2] == "logencoding":
|
||||
msg = "Current log encoding is set to:\n"
|
||||
msg = msg + response
|
||||
elif inC[2] in ("journalmatch", "addjournalmatch", "deljournalmatch"):
|
||||
if len(response) == 0:
|
||||
msg = "No journal match filter set"
|
||||
else:
|
||||
msg = "Current match filter:\n"
|
||||
msg += ' + '.join(" ".join(res) for res in response)
|
||||
elif inC[2] == "datepattern":
|
||||
msg = "Current date pattern set to: "
|
||||
if response is None:
|
||||
msg = msg + "Not set/required"
|
||||
elif response[0] is None:
|
||||
msg = msg + "%s" % response[1]
|
||||
else:
|
||||
msg = msg + "%s (%s)" % response
|
||||
elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"):
|
||||
if len(response) == 0:
|
||||
msg = "No IP address/network is ignored"
|
||||
|
@ -131,8 +155,30 @@ class Beautifier:
|
|||
msg = msg + "|- [" + str(c) + "]: " + ip + "\n"
|
||||
c += 1
|
||||
msg = msg + "`- [" + str(c) + "]: " + response[len(response)-1]
|
||||
elif inC[2] == "actions":
|
||||
if len(response) == 0:
|
||||
msg = "No actions for jail %s" % inC[1]
|
||||
else:
|
||||
msg = "The jail %s has the following actions:\n" % inC[1]
|
||||
msg += ", ".join(response)
|
||||
elif inC[2] == "actionproperties":
|
||||
if len(response) == 0:
|
||||
msg = "No properties for jail %s action %s" % (
|
||||
inC[1], inC[3])
|
||||
else:
|
||||
msg = "The jail %s action %s has the following " \
|
||||
"properties:\n" % (inC[1], inC[3])
|
||||
msg += ", ".join(response)
|
||||
elif inC[2] == "actionmethods":
|
||||
if len(response) == 0:
|
||||
msg = "No methods for jail %s action %s" % (
|
||||
inC[1], inC[3])
|
||||
else:
|
||||
msg = "The jail %s action %s has the following " \
|
||||
"methods:\n" % (inC[1], inC[3])
|
||||
msg += ", ".join(response)
|
||||
except Exception:
|
||||
logSys.warn("Beautifier error. Please report the error")
|
||||
logSys.warning("Beautifier error. Please report the error")
|
||||
logSys.error("Beautify " + `response` + " with " + `self.__inputCmd` +
|
||||
" failed")
|
||||
msg = msg + `response`
|
|
@ -24,11 +24,45 @@ __author__ = 'Yaroslav Halhenko'
|
|||
__copyright__ = 'Copyright (c) 2007 Yaroslav Halchenko'
|
||||
__license__ = 'GPL'
|
||||
|
||||
import logging, os
|
||||
from ConfigParser import SafeConfigParser
|
||||
import logging, os, sys
|
||||
|
||||
if sys.version_info >= (3,2): # pragma: no cover
|
||||
|
||||
# SafeConfigParser deprecated from Python 3.2 (renamed to ConfigParser)
|
||||
from configparser import ConfigParser as SafeConfigParser, \
|
||||
BasicInterpolation
|
||||
|
||||
# And interpolation of __name__ was simply removed, thus we need to
|
||||
# decorate default interpolator to handle it
|
||||
class BasicInterpolationWithName(BasicInterpolation):
|
||||
"""Decorator to bring __name__ interpolation back.
|
||||
|
||||
Original handling of __name__ was removed because of
|
||||
functional deficiencies: http://bugs.python.org/issue10489
|
||||
|
||||
commit v3.2a4-105-g61f2761
|
||||
Author: Lukasz Langa <lukasz@langa.pl>
|
||||
Date: Sun Nov 21 13:41:35 2010 +0000
|
||||
|
||||
Issue #10489: removed broken `__name__` support from configparser
|
||||
|
||||
But should be fine to reincarnate for our use case
|
||||
"""
|
||||
def _interpolate_some(self, parser, option, accum, rest, section, map,
|
||||
depth):
|
||||
if section and not (__name__ in map):
|
||||
map = map.copy() # just to be safe
|
||||
map['__name__'] = section
|
||||
return super(BasicInterpolationWithName, self)._interpolate_some(
|
||||
parser, option, accum, rest, section, map, depth)
|
||||
|
||||
else: # pragma: no cover
|
||||
from ConfigParser import SafeConfigParser
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['SafeConfigParserWithIncludes']
|
||||
|
||||
class SafeConfigParserWithIncludes(SafeConfigParser):
|
||||
"""
|
||||
|
@ -61,6 +95,15 @@ after = 1.conf
|
|||
|
||||
SECTION_NAME = "INCLUDES"
|
||||
|
||||
if sys.version_info >= (3,2):
|
||||
# overload constructor only for fancy new Python3's
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs = kwargs.copy()
|
||||
kwargs['interpolation'] = BasicInterpolationWithName()
|
||||
kwargs['inline_comment_prefixes'] = ";"
|
||||
super(SafeConfigParserWithIncludes, self).__init__(
|
||||
*args, **kwargs)
|
||||
|
||||
#@staticmethod
|
||||
def getIncludes(resource, seen = []):
|
||||
"""
|
||||
|
@ -73,7 +116,14 @@ after = 1.conf
|
|||
SCPWI = SafeConfigParserWithIncludes
|
||||
|
||||
parser = SafeConfigParser()
|
||||
parser.read(resource)
|
||||
try:
|
||||
if sys.version_info >= (3,2): # pragma: no cover
|
||||
parser.read(resource, encoding='utf-8')
|
||||
else:
|
||||
parser.read(resource)
|
||||
except UnicodeDecodeError, e:
|
||||
logSys.error("Error decoding config file '%s': %s" % (resource, e))
|
||||
return []
|
||||
|
||||
resourceDir = os.path.dirname(resource)
|
||||
|
||||
|
@ -104,5 +154,8 @@ after = 1.conf
|
|||
for filename in filenames:
|
||||
fileNamesFull += SafeConfigParserWithIncludes.getIncludes(filename)
|
||||
logSys.debug("Reading files: %s" % fileNamesFull)
|
||||
return SafeConfigParser.read(self, fileNamesFull)
|
||||
if sys.version_info >= (3,2): # pragma: no cover
|
||||
return SafeConfigParser.read(self, fileNamesFull, encoding='utf-8')
|
||||
else:
|
||||
return SafeConfigParser.read(self, fileNamesFull)
|
||||
|
|
@ -25,11 +25,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
|||
__license__ = "GPL"
|
||||
|
||||
import glob, logging, os
|
||||
from configparserinc import SafeConfigParserWithIncludes
|
||||
from ConfigParser import NoOptionError, NoSectionError
|
||||
|
||||
from .configparserinc import SafeConfigParserWithIncludes
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class ConfigReader(SafeConfigParserWithIncludes):
|
||||
|
||||
|
@ -53,7 +54,7 @@ class ConfigReader(SafeConfigParserWithIncludes):
|
|||
raise ValueError("Base configuration directory %s does not exist "
|
||||
% self._basedir)
|
||||
basename = os.path.join(self._basedir, filename)
|
||||
logSys.debug("Reading configs for %s under %s " % (basename, self._basedir))
|
||||
logSys.info("Reading configs for %s under %s " % (basename, self._basedir))
|
||||
config_files = [ basename + ".conf" ]
|
||||
|
||||
# possible further customizations under a .conf.d directory
|
||||
|
@ -112,14 +113,64 @@ class ConfigReader(SafeConfigParserWithIncludes):
|
|||
except NoSectionError, e:
|
||||
# No "Definition" section or wrong basedir
|
||||
logSys.error(e)
|
||||
return False
|
||||
values[option[1]] = option[2]
|
||||
# TODO: validate error handling here.
|
||||
except NoOptionError:
|
||||
if not option[2] is None:
|
||||
logSys.warn("'%s' not defined in '%s'. Using default one: %r"
|
||||
logSys.warning("'%s' not defined in '%s'. Using default one: %r"
|
||||
% (option[1], sec, option[2]))
|
||||
values[option[1]] = option[2]
|
||||
except ValueError:
|
||||
logSys.warn("Wrong value for '" + option[1] + "' in '" + sec +
|
||||
logSys.warning("Wrong value for '" + option[1] + "' in '" + sec +
|
||||
"'. Using default one: '" + `option[2]` + "'")
|
||||
values[option[1]] = option[2]
|
||||
return values
|
||||
|
||||
class DefinitionInitConfigReader(ConfigReader):
|
||||
"""Config reader for files with options grouped in [Definition] and
|
||||
[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.
|
||||
"""
|
||||
|
||||
_configOpts = []
|
||||
|
||||
def __init__(self, file_, jailName, initOpts, **kwargs):
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
self.setFile(file_)
|
||||
self.setJailName(jailName)
|
||||
self._initOpts = initOpts
|
||||
|
||||
def setFile(self, fileName):
|
||||
self._file = fileName
|
||||
self._initOpts = {}
|
||||
|
||||
def getFile(self):
|
||||
return self._file
|
||||
|
||||
def setJailName(self, jailName):
|
||||
self._jailName = jailName
|
||||
|
||||
def getJailName(self):
|
||||
return self._jailName
|
||||
|
||||
def read(self):
|
||||
return ConfigReader.read(self, self._file)
|
||||
|
||||
# needed for fail2ban-regex that doesn't need fancy directories
|
||||
def readexplicit(self):
|
||||
return SafeConfigParserWithIncludes.read(self, self._file)
|
||||
|
||||
def getOptions(self, pOpts):
|
||||
self._opts = ConfigReader.getOptions(
|
||||
self, "Definition", self._configOpts, pOpts)
|
||||
|
||||
if self.has_section("Init"):
|
||||
for opt in self.options("Init"):
|
||||
if not self._initOpts.has_key(opt):
|
||||
self._initOpts[opt] = self.get("Init", opt)
|
||||
|
||||
def convert(self):
|
||||
raise NotImplementedError
|
|
@ -25,12 +25,13 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
|||
__license__ = "GPL"
|
||||
|
||||
import logging
|
||||
from configreader import ConfigReader
|
||||
from fail2banreader import Fail2banReader
|
||||
from jailsreader import JailsReader
|
||||
|
||||
from .configreader import ConfigReader
|
||||
from .fail2banreader import Fail2banReader
|
||||
from .jailsreader import JailsReader
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class Configurator:
|
||||
|
|
@ -26,11 +26,21 @@ __license__ = "GPL"
|
|||
|
||||
#from cPickle import dumps, loads, HIGHEST_PROTOCOL
|
||||
from pickle import dumps, loads, HIGHEST_PROTOCOL
|
||||
import socket
|
||||
import socket, sys
|
||||
|
||||
if sys.version_info >= (3,):
|
||||
# b"" causes SyntaxError in python <= 2.5, so below implements equivalent
|
||||
EMPTY_BYTES = bytes("", encoding="ascii")
|
||||
else:
|
||||
# python 2.x, string type is equivalent to bytes.
|
||||
EMPTY_BYTES = ""
|
||||
|
||||
class CSocket:
|
||||
|
||||
END_STRING = "<F2B_END_COMMAND>"
|
||||
if sys.version_info >= (3,):
|
||||
END_STRING = bytes("<F2B_END_COMMAND>", encoding='ascii')
|
||||
else:
|
||||
END_STRING = "<F2B_END_COMMAND>"
|
||||
|
||||
def __init__(self, sock = "/var/run/fail2ban/fail2ban.sock"):
|
||||
# Create an INET, STREAMing socket
|
||||
|
@ -49,7 +59,7 @@ class CSocket:
|
|||
|
||||
#@staticmethod
|
||||
def receive(sock):
|
||||
msg = ''
|
||||
msg = EMPTY_BYTES
|
||||
while msg.rfind(CSocket.END_STRING) == -1:
|
||||
chunk = sock.recv(6)
|
||||
if chunk == '':
|
|
@ -25,10 +25,11 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
|||
__license__ = "GPL"
|
||||
|
||||
import logging
|
||||
from configreader import ConfigReader
|
||||
|
||||
from .configreader import ConfigReader
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class Fail2banReader(ConfigReader):
|
||||
|
||||
|
@ -44,8 +45,10 @@ class Fail2banReader(ConfigReader):
|
|||
return ConfigReader.getOptions(self, "Definition", opts)
|
||||
|
||||
def getOptions(self):
|
||||
opts = [["int", "loglevel", 1],
|
||||
["string", "logtarget", "STDERR"]]
|
||||
opts = [["int", "loglevel", "INFO" ],
|
||||
["string", "logtarget", "STDERR"],
|
||||
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
|
||||
["int", "dbpurgeage", 86400]]
|
||||
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
|
||||
|
||||
def convert(self):
|
||||
|
@ -55,5 +58,10 @@ class Fail2banReader(ConfigReader):
|
|||
stream.append(["set", "loglevel", self.__opts[opt]])
|
||||
elif opt == "logtarget":
|
||||
stream.append(["set", "logtarget", self.__opts[opt]])
|
||||
return stream
|
||||
elif opt == "dbfile":
|
||||
stream.append(["set", "dbfile", self.__opts[opt]])
|
||||
elif opt == "dbpurgeage":
|
||||
stream.append(["set", "dbpurgeage", self.__opts[opt]])
|
||||
# Ensure logtarget/level set first so any db errors are captured
|
||||
return sorted(stream, reverse=True)
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging, os, shlex
|
||||
|
||||
from .configreader import ConfigReader, DefinitionInitConfigReader
|
||||
from ..server.action import CommandAction
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class FilterReader(DefinitionInitConfigReader):
|
||||
|
||||
_configOpts = [
|
||||
["string", "ignoreregex", ""],
|
||||
["string", "failregex", ""],
|
||||
]
|
||||
|
||||
def read(self):
|
||||
return ConfigReader.read(self, os.path.join("filter.d", self._file))
|
||||
|
||||
def convert(self):
|
||||
stream = list()
|
||||
combinedopts = dict(list(self._opts.items()) + list(self._initOpts.items()))
|
||||
opts = CommandAction.substituteRecursiveTags(combinedopts)
|
||||
if not opts:
|
||||
raise ValueError('recursive tag definitions unable to be resolved')
|
||||
for opt, value in opts.iteritems():
|
||||
if opt == "failregex":
|
||||
for regex in value.split('\n'):
|
||||
# Do not send a command if the rule is empty.
|
||||
if regex != '':
|
||||
stream.append(["set", self._jailName, "addfailregex", regex])
|
||||
elif opt == "ignoreregex":
|
||||
for regex in value.split('\n'):
|
||||
# Do not send a command if the rule is empty.
|
||||
if regex != '':
|
||||
stream.append(["set", self._jailName, "addignoreregex", regex])
|
||||
if self._initOpts:
|
||||
if 'maxlines' in self._initOpts:
|
||||
# We warn when multiline regex is used without maxlines > 1
|
||||
# therefore keep sure we set this option first.
|
||||
stream.insert(0, ["set", self._jailName, "maxlines", self._initOpts["maxlines"]])
|
||||
if 'datepattern' in self._initOpts:
|
||||
stream.append(["set", self._jailName, "datepattern", self._initOpts["datepattern"]])
|
||||
# Do not send a command if the match is empty.
|
||||
if self._initOpts.get("journalmatch", '') != '':
|
||||
for match in self._initOpts["journalmatch"].split("\n"):
|
||||
stream.append(
|
||||
["set", self._jailName, "addjournalmatch"] +
|
||||
shlex.split(match))
|
||||
return stream
|
||||
|
|
@ -25,17 +25,20 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
|||
__license__ = "GPL"
|
||||
|
||||
import logging, re, glob, os.path
|
||||
import json
|
||||
|
||||
from configreader import ConfigReader
|
||||
from filterreader import FilterReader
|
||||
from actionreader import ActionReader
|
||||
from .configreader import ConfigReader
|
||||
from .filterreader import FilterReader
|
||||
from .actionreader import ActionReader
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class JailReader(ConfigReader):
|
||||
|
||||
actionCRE = re.compile("^([\w_.-]+)(?:\[(.*)\])?$")
|
||||
optionCRE = re.compile("^((?:\w|-|_|\.)+)(?:\[(.*)\])?$")
|
||||
optionExtractRE = re.compile(
|
||||
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,]*))(?:,|$)')
|
||||
|
||||
def __init__(self, name, force_enable=False, **kwargs):
|
||||
ConfigReader.__init__(self, **kwargs)
|
||||
|
@ -45,7 +48,8 @@ class JailReader(ConfigReader):
|
|||
self.__actions = list()
|
||||
self.__opts = None
|
||||
|
||||
def getRawOptions(self):
|
||||
@property
|
||||
def options(self):
|
||||
return self.__opts
|
||||
|
||||
def setName(self, value):
|
||||
|
@ -55,7 +59,13 @@ class JailReader(ConfigReader):
|
|||
return self.__name
|
||||
|
||||
def read(self):
|
||||
return ConfigReader.read(self, "jail")
|
||||
out = ConfigReader.read(self, "jail")
|
||||
# Before returning -- verify that requested section
|
||||
# exists at all
|
||||
if not (self.__name in self.sections()):
|
||||
raise ValueError("Jail %r was not found among available"
|
||||
% self.__name)
|
||||
return out
|
||||
|
||||
def isEnabled(self):
|
||||
return self.__force_enable or ( self.__opts and self.__opts["enabled"] )
|
||||
|
@ -77,6 +87,7 @@ class JailReader(ConfigReader):
|
|||
def getOptions(self):
|
||||
opts = [["bool", "enabled", "false"],
|
||||
["string", "logpath", "/var/log/messages"],
|
||||
["string", "logencoding", "auto"],
|
||||
["string", "backend", "auto"],
|
||||
["int", "maxretry", 3],
|
||||
["int", "findtime", 600],
|
||||
|
@ -95,8 +106,10 @@ class JailReader(ConfigReader):
|
|||
if self.isEnabled():
|
||||
# Read filter
|
||||
if self.__opts["filter"]:
|
||||
self.__filter = FilterReader(self.__opts["filter"], self.__name,
|
||||
basedir=self.getBaseDir())
|
||||
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)
|
||||
|
@ -105,27 +118,40 @@ class JailReader(ConfigReader):
|
|||
return False
|
||||
else:
|
||||
self.__filter = None
|
||||
logSys.warn("No filter set for jail %s" % self.__name)
|
||||
logSys.warning("No filter set for jail %s" % self.__name)
|
||||
|
||||
# Read action
|
||||
for act in self.__opts["action"].split('\n'):
|
||||
try:
|
||||
if not act: # skip empty actions
|
||||
continue
|
||||
splitAct = JailReader.splitAction(act)
|
||||
action = ActionReader(splitAct, self.__name, basedir=self.getBaseDir())
|
||||
ret = action.read()
|
||||
if ret:
|
||||
action.getOptions(self.__opts)
|
||||
self.__actions.append(action)
|
||||
actName, actOpt = JailReader.extractOptions(act)
|
||||
if actName.endswith(".py"):
|
||||
self.__actions.append([
|
||||
"set",
|
||||
self.__name,
|
||||
"addaction",
|
||||
actOpt.pop("actname", os.path.splitext(actName)[0]),
|
||||
os.path.join(
|
||||
self.getBaseDir(), "action.d", actName),
|
||||
json.dumps(actOpt),
|
||||
])
|
||||
else:
|
||||
raise AttributeError("Unable to read action")
|
||||
action = ActionReader(
|
||||
actName, self.__name, actOpt,
|
||||
basedir=self.getBaseDir())
|
||||
ret = action.read()
|
||||
if ret:
|
||||
action.getOptions(self.__opts)
|
||||
self.__actions.append(action)
|
||||
else:
|
||||
raise AttributeError("Unable to read action")
|
||||
except Exception, e:
|
||||
logSys.error("Error in action definition " + act)
|
||||
logSys.debug("Caught exception: %s" % (e,))
|
||||
return False
|
||||
if not len(self.__actions):
|
||||
logSys.warn("No actions were defined for %s" % self.__name)
|
||||
logSys.warning("No actions were defined for %s" % self.__name)
|
||||
return True
|
||||
|
||||
def convert(self, allow_no_files=False):
|
||||
|
@ -140,18 +166,24 @@ class JailReader(ConfigReader):
|
|||
|
||||
stream = []
|
||||
for opt in self.__opts:
|
||||
if opt == "logpath":
|
||||
if opt == "logpath" and \
|
||||
self.__opts.get('backend', None) != "systemd":
|
||||
found_files = 0
|
||||
for path in self.__opts[opt].split("\n"):
|
||||
path = path.rsplit(" ", 1)
|
||||
path, tail = path if len(path) > 1 else (path[0], "head")
|
||||
pathList = JailReader._glob(path)
|
||||
if len(pathList) == 0:
|
||||
logSys.error("No file(s) found for glob %s" % path)
|
||||
for p in pathList:
|
||||
found_files += 1
|
||||
stream.append(["set", self.__name, "addlogpath", p])
|
||||
stream.append(
|
||||
["set", self.__name, "addlogpath", p, tail])
|
||||
if not (found_files or allow_no_files):
|
||||
raise ValueError(
|
||||
"Have not found any log file for %s jail" % self.__name)
|
||||
elif opt == "logencoding":
|
||||
stream.append(["set", self.__name, "logencoding", self.__opts[opt]])
|
||||
elif opt == "backend":
|
||||
backend = self.__opts[opt]
|
||||
elif opt == "maxretry":
|
||||
|
@ -179,56 +211,26 @@ class JailReader(ConfigReader):
|
|||
if self.__filter:
|
||||
stream.extend(self.__filter.convert())
|
||||
for action in self.__actions:
|
||||
stream.extend(action.convert())
|
||||
if isinstance(action, ConfigReader):
|
||||
stream.extend(action.convert())
|
||||
else:
|
||||
stream.append(action)
|
||||
stream.insert(0, ["add", self.__name, backend])
|
||||
return stream
|
||||
|
||||
#@staticmethod
|
||||
def splitAction(action):
|
||||
m = JailReader.actionCRE.match(action)
|
||||
d = dict()
|
||||
try:
|
||||
mgroups = m.groups()
|
||||
except AttributeError:
|
||||
raise ValueError("While reading action %s we should have got 1 or "
|
||||
"2 groups. Got: 0" % action)
|
||||
if len(mgroups) == 2:
|
||||
action_name, action_opts = mgroups
|
||||
elif len(mgroups) == 1: # pragma: nocover - unreachable - .* on second group always matches
|
||||
action_name, action_opts = mgroups[0], None
|
||||
else: # pragma: nocover - unreachable - regex only can capture 2 groups
|
||||
raise ValueError("While reading action %s we should have got up to "
|
||||
"2 groups. Got: %r" % (action, mgroups))
|
||||
if not action_opts is None:
|
||||
# Huge bad hack :( This method really sucks. TODO Reimplement it.
|
||||
actions = ""
|
||||
escapeChar = None
|
||||
allowComma = False
|
||||
for c in action_opts:
|
||||
if c in ('"', "'") and not allowComma:
|
||||
# Start
|
||||
escapeChar = c
|
||||
allowComma = True
|
||||
elif c == escapeChar:
|
||||
# End
|
||||
escapeChar = None
|
||||
allowComma = False
|
||||
else:
|
||||
if c == ',' and allowComma:
|
||||
actions += "<COMMA>"
|
||||
else:
|
||||
actions += c
|
||||
|
||||
# Split using ,
|
||||
actionsSplit = actions.split(',')
|
||||
# Replace the tag <COMMA> with ,
|
||||
actionsSplit = [n.replace("<COMMA>", ',') for n in actionsSplit]
|
||||
|
||||
for param in actionsSplit:
|
||||
p = param.split('=')
|
||||
try:
|
||||
d[p[0].strip()] = p[1].strip()
|
||||
except IndexError:
|
||||
logSys.error("Invalid argument %s in '%s'" % (p, action_opts))
|
||||
return [action_name, d]
|
||||
splitAction = staticmethod(splitAction)
|
||||
def extractOptions(option):
|
||||
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):
|
||||
opt = optmatch.group(1)
|
||||
value = [
|
||||
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)
|
|
@ -25,11 +25,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
|||
__license__ = "GPL"
|
||||
|
||||
import logging
|
||||
from configreader import ConfigReader
|
||||
from jailreader import JailReader
|
||||
|
||||
from .configreader import ConfigReader
|
||||
from .jailreader import JailReader
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.client.config")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class JailsReader(ConfigReader):
|
||||
|
||||
|
@ -45,7 +46,8 @@ class JailsReader(ConfigReader):
|
|||
self.__jails = list()
|
||||
self.__force_enable = force_enable
|
||||
|
||||
def getJails(self):
|
||||
@property
|
||||
def jails(self):
|
||||
return self.__jails
|
||||
|
||||
def read(self):
|
||||
|
@ -65,6 +67,8 @@ class JailsReader(ConfigReader):
|
|||
# Get the options of all jails.
|
||||
parse_status = True
|
||||
for sec in sections:
|
||||
if sec == 'INCLUDES':
|
||||
continue
|
||||
jail = JailReader(sec, basedir=self.getBaseDir(),
|
||||
force_enable=self.__force_enable)
|
||||
jail.read()
|
|
@ -29,7 +29,7 @@ __license__ = "GPL"
|
|||
class DuplicateJailException(Exception):
|
||||
pass
|
||||
|
||||
class UnknownJailException(Exception):
|
||||
class UnknownJailException(KeyError):
|
||||
pass
|
||||
|
||||
|
|
@ -39,11 +39,16 @@ protocol = [
|
|||
["ping", "tests if the server is alive"],
|
||||
["help", "return this output"],
|
||||
['', "LOGGING", ""],
|
||||
["set loglevel <LEVEL>", "sets logging level to <LEVEL>. 0 is minimal, 4 is debug"],
|
||||
["set loglevel <LEVEL>", "sets logging level to <LEVEL>. Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG"],
|
||||
["get loglevel", "gets the logging level"],
|
||||
["set logtarget <TARGET>", "sets logging target to <TARGET>. 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 <FILE>", "set the location of fail2ban persistent datastore. Set to \"None\" to disable"],
|
||||
["get dbfile", "get the location of fail2ban persistent datastore"],
|
||||
["set dbpurgeage <SECONDS>", "sets the max age in <SECONDS> that history of bans will be kept"],
|
||||
["get dbpurgeage", "gets the max age in seconds that history of bans will be kept"],
|
||||
['', "JAIL CONTROL", ""],
|
||||
["add <JAIL> <BACKEND>", "creates <JAIL> using <BACKEND>"],
|
||||
["start <JAIL>", "starts the jail <JAIL>"],
|
||||
|
@ -53,8 +58,11 @@ protocol = [
|
|||
["set <JAIL> idle on|off", "sets the idle state of <JAIL>"],
|
||||
["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"],
|
||||
["set <JAIL> delignoreip <IP>", "removes <IP> from the ignore list of <JAIL>"],
|
||||
["set <JAIL> addlogpath <FILE>", "adds <FILE> to the monitoring list of <JAIL>"],
|
||||
["set <JAIL> addlogpath <FILE> ['tail']", "adds <FILE> to the monitoring list of <JAIL>, optionally starting at the 'tail' of the file (default 'head')."],
|
||||
["set <JAIL> dellogpath <FILE>", "removes <FILE> from the monitoring list of <JAIL>"],
|
||||
["set <JAIL> logencoding <ENCODING>", "sets the <ENCODING> of the log files for <JAIL>"],
|
||||
["set <JAIL> addjournalmatch <MATCH>", "adds <MATCH> to the journal filter of <JAIL>"],
|
||||
["set <JAIL> deljournalmatch <MATCH>", "removes <MATCH> from the journal filter of <JAIL>"],
|
||||
["set <JAIL> addfailregex <REGEX>", "adds the regular expression <REGEX> which must match failures for <JAIL>"],
|
||||
["set <JAIL> delfailregex <INDEX>", "removes the regular expression at <INDEX> for failregex"],
|
||||
["set <JAIL> ignorecommand <VALUE>", "sets ignorecommand of <JAIL>"],
|
||||
|
@ -62,36 +70,50 @@ protocol = [
|
|||
["set <JAIL> delignoreregex <INDEX>", "removes the regular expression at <INDEX> for ignoreregex"],
|
||||
["set <JAIL> findtime <TIME>", "sets the number of seconds <TIME> for which the filter will look back for <JAIL>"],
|
||||
["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"],
|
||||
["set <JAIL> datepattern <PATTERN>", "sets the <PATTERN> used to match date/times for <JAIL>"],
|
||||
["set <JAIL> usedns <VALUE>", "sets the usedns mode for <JAIL>"],
|
||||
["set <JAIL> banip <IP>", "manually Ban <IP> for <JAIL>"],
|
||||
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"],
|
||||
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
|
||||
["set <JAIL> addaction <ACT>", "adds a new action named <NAME> for <JAIL>"],
|
||||
["set <JAIL> delaction <ACT>", "removes the action <NAME> from <JAIL>"],
|
||||
["set <JAIL> setcinfo <ACT> <KEY> <VALUE>", "sets <VALUE> for <KEY> of the action <NAME> for <JAIL>"],
|
||||
["set <JAIL> delcinfo <ACT> <KEY>", "removes <KEY> for the action <NAME> for <JAIL>"],
|
||||
["set <JAIL> actionstart <ACT> <CMD>", "sets the start command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> actionstop <ACT> <CMD>", "sets the stop command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> actioncheck <ACT> <CMD>", "sets the check command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> actionban <ACT> <CMD>", "sets the ban command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> actionunban <ACT> <CMD>", "sets the unban command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"],
|
||||
["set <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]", "adds a new action named <NAME> for <JAIL>. Optionally for a Python based action, a <PYTHONFILE> and <JSONKWARGS> can be specified, else will be a Command Action"],
|
||||
["set <JAIL> delaction <ACT>", "removes the action <ACT> from <JAIL>"],
|
||||
["", "COMMAND ACTION CONFIGURATION", ""],
|
||||
["set <JAIL> action <ACT> actionstart <CMD>", "sets the start command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> action <ACT> actionstop <CMD>", "sets the stop command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> action <ACT> actioncheck <CMD>", "sets the check command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> action <ACT> actionban <CMD>", "sets the ban command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> action <ACT> actionunban <CMD>", "sets the unban command <CMD> of the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> action <ACT> timeout <TIMEOUT>", "sets <TIMEOUT> as the command timeout in seconds for the action <ACT> for <JAIL>"],
|
||||
["", "GENERAL ACTION CONFIGURATION", ""],
|
||||
["set <JAIL> action <ACT> <PROPERTY> <VALUE>", "sets the <VALUE> of <PROPERTY> for the action <ACT> for <JAIL>"],
|
||||
["set <JAIL> action <ACT> <METHOD>[ <JSONKWARGS>]", "calls the <METHOD> with <JSONKWARGS> for the action <ACT> for <JAIL>"],
|
||||
['', "JAIL INFORMATION", ""],
|
||||
["get <JAIL> logpath", "gets the list of the monitored files for <JAIL>"],
|
||||
["get <JAIL> logencoding", "gets the encoding of the log files for <JAIL>"],
|
||||
["get <JAIL> journalmatch", "gets the journal filter match for <JAIL>"],
|
||||
["get <JAIL> ignoreip", "gets the list of ignored IP addresses for <JAIL>"],
|
||||
["get <JAIL> ignorecommand", "gets ignorecommand of <JAIL>"],
|
||||
["get <JAIL> failregex", "gets the list of regular expressions which matches the failures for <JAIL>"],
|
||||
["get <JAIL> ignoreregex", "gets the list of regular expressions which matches patterns to ignore for <JAIL>"],
|
||||
["get <JAIL> findtime", "gets the time for which the filter will look back for failures for <JAIL>"],
|
||||
["get <JAIL> bantime", "gets the time a host is banned for <JAIL>"],
|
||||
["get <JAIL> datepattern", "gets the patern used to match date/times for <JAIL>"],
|
||||
["get <JAIL> usedns", "gets the usedns setting for <JAIL>"],
|
||||
["get <JAIL> maxretry", "gets the number of failures allowed for <JAIL>"],
|
||||
["get <JAIL> addaction", "gets the last action which has been added for <JAIL>"],
|
||||
["get <JAIL> actionstart <ACT>", "gets the start command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> actionstop <ACT>", "gets the stop command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> actioncheck <ACT>", "gets the check command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> actionban <ACT>", "gets the ban command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> actionunban <ACT>", "gets the unban command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> cinfo <ACT> <KEY>", "gets the value for <KEY> for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> maxlines", "gets the number of lines to buffer for <JAIL>"],
|
||||
["get <JAIL> actions", "gets a list of actions for <JAIL>"],
|
||||
["", "COMMAND ACTION INFORMATION",""],
|
||||
["get <JAIL> action <ACT> actionstart", "gets the start command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> action <ACT> actionstop", "gets the stop command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> action <ACT> actioncheck", "gets the check command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> action <ACT> actionban", "gets the ban command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> action <ACT> actionunban", "gets the unban command for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> action <ACT> timeout", "gets the command timeout in seconds for the action <ACT> for <JAIL>"],
|
||||
["", "GENERAL ACTION INFORMATION", ""],
|
||||
["get <JAIL> actionproperties <ACT>", "gets a list of properties for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> actionmethods <ACT>", "gets a list of methods for the action <ACT> for <JAIL>"],
|
||||
["get <JAIL> action <ACT> <PROPERTY>", "gets the value of <PROPERTY> for the action <ACT> for <JAIL>"],
|
||||
]
|
||||
|
||||
##
|
||||
|
@ -108,12 +130,14 @@ def printFormatted():
|
|||
print
|
||||
firstHeading = True
|
||||
first = True
|
||||
for n in textwrap.wrap(m[1], WIDTH):
|
||||
if len(m[0]) >= MARGIN:
|
||||
m[1] = ' ' * WIDTH + m[1]
|
||||
for n in textwrap.wrap(m[1], WIDTH, drop_whitespace=False):
|
||||
if first:
|
||||
line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n
|
||||
line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n.strip()
|
||||
first = False
|
||||
else:
|
||||
line = ' ' * (INDENT + MARGIN) + n
|
||||
line = ' ' * (INDENT + MARGIN) + n.strip()
|
||||
print line
|
||||
|
||||
##
|
|
@ -0,0 +1,583 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Cyril Jaquier and Fail2Ban Contributors"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging, os, subprocess, time, signal, tempfile
|
||||
import threading, re
|
||||
from abc import ABCMeta
|
||||
from collections import MutableMapping
|
||||
#from subprocess import call
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
# Create a lock for running system commands
|
||||
_cmd_lock = threading.Lock()
|
||||
|
||||
# Some hints on common abnormal exit codes
|
||||
_RETCODE_HINTS = {
|
||||
127: '"Command not found". Make sure that all commands in %(realCmd)r '
|
||||
'are in the PATH of fail2ban-server process '
|
||||
'(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). '
|
||||
'You may want to start '
|
||||
'"fail2ban-server -f" separately, initiate it with '
|
||||
'"fail2ban-client reload" in another shell session and observe if '
|
||||
'additional informative error messages appear in the terminals.'
|
||||
}
|
||||
|
||||
# Dictionary to lookup signal name from number
|
||||
signame = dict((num, name)
|
||||
for name, num in signal.__dict__.iteritems() if name.startswith("SIG"))
|
||||
|
||||
class CallingMap(MutableMapping):
|
||||
"""A Mapping type which returns the result of callable values.
|
||||
|
||||
`CallingMap` behaves similar to a standard python dictionary,
|
||||
with the exception that any values which are callable, are called
|
||||
and the result is returned as the value.
|
||||
No error handling is in place, such that any errors raised in the
|
||||
callable will raised as usual.
|
||||
Actual dictionary is stored in property `data`, and can be accessed
|
||||
to obtain original callable values.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
data : dict
|
||||
The dictionary data which can be accessed to obtain items
|
||||
without callable values being called.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.data = dict(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = self.data[key]
|
||||
if callable(value):
|
||||
return value()
|
||||
else:
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.data[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.data[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
class ActionBase(object):
|
||||
"""An abstract base class for actions in Fail2Ban.
|
||||
|
||||
Action Base is a base definition of what methods need to be in
|
||||
place to create a Python based action for Fail2Ban. This class can
|
||||
be inherited from to ease implementation.
|
||||
Required methods:
|
||||
|
||||
- __init__(jail, name)
|
||||
- start()
|
||||
- stop()
|
||||
- ban(aInfo)
|
||||
- unban(aInfo)
|
||||
|
||||
Called when action is created, but before the jail/actions is
|
||||
started. This should carry out necessary methods to initialise
|
||||
the action but not "start" the action.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail in which the action belongs to.
|
||||
name : str
|
||||
Name assigned to the action.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Any additional arguments specified in `jail.conf` or passed
|
||||
via `fail2ban-client` will be passed as keyword arguments.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
required = (
|
||||
"start",
|
||||
"stop",
|
||||
"ban",
|
||||
"unban",
|
||||
)
|
||||
for method in required:
|
||||
if not callable(getattr(C, method, None)):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __init__(self, jail, name):
|
||||
self._jail = jail
|
||||
self._name = name
|
||||
self._logSys = logging.getLogger(
|
||||
'%s.%s' % (__name__, self.__class__.__name__))
|
||||
|
||||
def start(self):
|
||||
"""Executed when the jail/action is started.
|
||||
"""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""Executed when the jail/action is stopped.
|
||||
"""
|
||||
pass
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Executed when a ban occurs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
pass
|
||||
|
||||
def unban(self, aInfo):
|
||||
"""Executed when a ban expires.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
pass
|
||||
|
||||
class CommandAction(ActionBase):
|
||||
"""A action which executes OS shell commands.
|
||||
|
||||
This is the default type of action which Fail2Ban uses.
|
||||
|
||||
Default sets all commands for actions as empty string, such
|
||||
no command is executed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail in which the action belongs to.
|
||||
name : str
|
||||
Name assigned to the action.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
actionban
|
||||
actionstart
|
||||
actionstop
|
||||
actionunban
|
||||
timeout
|
||||
"""
|
||||
|
||||
def __init__(self, jail, name):
|
||||
super(CommandAction, self).__init__(jail, name)
|
||||
self.timeout = 60
|
||||
## Command executed in order to initialize the system.
|
||||
self.actionstart = ''
|
||||
## Command executed when an IP address gets banned.
|
||||
self.actionban = ''
|
||||
## Command executed when an IP address gets removed.
|
||||
self.actionunban = ''
|
||||
## Command executed in order to check requirements.
|
||||
self.actioncheck = ''
|
||||
## Command executed in order to stop the system.
|
||||
self.actionstop = ''
|
||||
self._logSys.debug("Created %s" % self.__class__)
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
return NotImplemented # Standard checks
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
"""Time out period in seconds for execution of commands.
|
||||
"""
|
||||
return self._timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, timeout):
|
||||
self._timeout = int(timeout)
|
||||
self._logSys.debug("Set action %s timeout = %i" %
|
||||
(self._name, self.timeout))
|
||||
|
||||
@property
|
||||
def _properties(self):
|
||||
"""A dictionary of the actions properties.
|
||||
|
||||
This is used to subsitute "tags" in the commands.
|
||||
"""
|
||||
return dict(
|
||||
(key, getattr(self, key))
|
||||
for key in dir(self)
|
||||
if not key.startswith("_") and not callable(getattr(self, key)))
|
||||
|
||||
@property
|
||||
def actionstart(self):
|
||||
"""The command executed on start of the jail/action.
|
||||
"""
|
||||
return self._actionstart
|
||||
|
||||
@actionstart.setter
|
||||
def actionstart(self, value):
|
||||
self._actionstart = value
|
||||
self._logSys.debug("Set actionstart = %s" % value)
|
||||
|
||||
def start(self):
|
||||
"""Executes the "actionstart" command.
|
||||
|
||||
Replace the tags in the action command with actions properties
|
||||
and executes the resulting command.
|
||||
"""
|
||||
if (self._properties and
|
||||
not self.substituteRecursiveTags(self._properties)):
|
||||
self._logSys.error(
|
||||
"properties contain self referencing definitions "
|
||||
"and cannot be resolved")
|
||||
raise RuntimeError("Error starting action")
|
||||
startCmd = self.replaceTag(self.actionstart, self._properties)
|
||||
if not self.executeCmd(startCmd, self.timeout):
|
||||
raise RuntimeError("Error starting action")
|
||||
|
||||
@property
|
||||
def actionban(self):
|
||||
"""The command used when a ban occurs.
|
||||
"""
|
||||
return self._actionban
|
||||
|
||||
@actionban.setter
|
||||
def actionban(self, value):
|
||||
self._actionban = value
|
||||
self._logSys.debug("Set actionban = %s" % value)
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Executes the "actionban" command.
|
||||
|
||||
Replaces the tags in the action command with actions properties
|
||||
and ban information, and executes the resulting command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
if not self._processCmd(self.actionban, aInfo):
|
||||
raise RuntimeError("Error banning %(ip)s" % aInfo)
|
||||
|
||||
@property
|
||||
def actionunban(self):
|
||||
"""The command used when an unban occurs.
|
||||
"""
|
||||
return self._actionunban
|
||||
|
||||
@actionunban.setter
|
||||
def actionunban(self, value):
|
||||
self._actionunban = value
|
||||
self._logSys.debug("Set actionunban = %s" % value)
|
||||
|
||||
def unban(self, aInfo):
|
||||
"""Executes the "actionunban" command.
|
||||
|
||||
Replaces the tags in the action command with actions properties
|
||||
and ban information, and executes the resulting command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
if not self._processCmd(self.actionunban, aInfo):
|
||||
raise RuntimeError("Error unbanning %(ip)s" % aInfo)
|
||||
|
||||
@property
|
||||
def actioncheck(self):
|
||||
"""The command used to check the environment.
|
||||
|
||||
This is used prior to a ban taking place to ensure the
|
||||
environment is appropriate. If this check fails, `stop` and
|
||||
`start` is executed prior to the check being called again.
|
||||
"""
|
||||
return self._actioncheck
|
||||
|
||||
@actioncheck.setter
|
||||
def actioncheck(self, value):
|
||||
self._actioncheck = value
|
||||
self._logSys.debug("Set actioncheck = %s" % value)
|
||||
|
||||
@property
|
||||
def actionstop(self):
|
||||
"""The command executed when the jail/actions stops.
|
||||
"""
|
||||
return self._actionstop
|
||||
|
||||
@actionstop.setter
|
||||
def actionstop(self, value):
|
||||
self._actionstop = value
|
||||
self._logSys.debug("Set actionstop = %s" % value)
|
||||
|
||||
def stop(self):
|
||||
"""Executes the "actionstop" command.
|
||||
|
||||
Replaces the tags in the action command with actions properties
|
||||
and executes the resulting command.
|
||||
"""
|
||||
stopCmd = self.replaceTag(self.actionstop, self._properties)
|
||||
if not self.executeCmd(stopCmd, self.timeout):
|
||||
raise RuntimeError("Error stopping action")
|
||||
|
||||
@staticmethod
|
||||
def substituteRecursiveTags(tags):
|
||||
"""Sort out tag definitions within other tags.
|
||||
|
||||
so: becomes:
|
||||
a = 3 a = 3
|
||||
b = <a>_3 b = 3_3
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tags : dict
|
||||
Dictionary of tags(keys) and their values.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Dictionary of tags(keys) and their values, with tags
|
||||
within the values recursively replaced.
|
||||
"""
|
||||
t = re.compile(r'<([^ >]+)>')
|
||||
for tag, value in tags.iteritems():
|
||||
value = str(value)
|
||||
m = t.search(value)
|
||||
done = []
|
||||
#logSys.log(5, 'TAG: %s, value: %s' % (tag, value))
|
||||
while m:
|
||||
found_tag = m.group(1)
|
||||
#logSys.log(5, 'found: %s' % found_tag)
|
||||
if found_tag == tag or found_tag in done:
|
||||
# recursive definitions are bad
|
||||
#logSys.log(5, 'recursion fail tag: %s value: %s' % (tag, value) )
|
||||
return False
|
||||
else:
|
||||
if tags.has_key(found_tag):
|
||||
value = value.replace('<%s>' % found_tag , tags[found_tag])
|
||||
#logSys.log(5, 'value now: %s' % value)
|
||||
done.append(found_tag)
|
||||
m = t.search(value, m.start())
|
||||
else:
|
||||
# Missing tags are ok so we just continue on searching.
|
||||
# cInfo can contain aInfo elements like <HOST> and valid shell
|
||||
# constructs like <STDIN>.
|
||||
m = t.search(value, m.start() + 1)
|
||||
#logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value))
|
||||
tags[tag] = value
|
||||
return tags
|
||||
|
||||
@staticmethod
|
||||
def escapeTag(value):
|
||||
"""Escape characters which may be used for command injection.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : str
|
||||
A string of which characters will be escaped.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
`value` with certain characters escaped.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The following characters are escaped::
|
||||
|
||||
\\#&;`|*?~<>^()[]{}$'"
|
||||
|
||||
"""
|
||||
for c in '\\#&;`|*?~<>^()[]{}$\'"':
|
||||
if c in value:
|
||||
value = value.replace(c, '\\' + c)
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def replaceTag(cls, query, aInfo):
|
||||
"""Replaces tags in `query` with property values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
query : str
|
||||
String with tags.
|
||||
aInfo : dict
|
||||
Tags(keys) and associated values for substitution in query.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
`query` string with tags replaced.
|
||||
"""
|
||||
string = query
|
||||
for tag in aInfo:
|
||||
if "<%s>" % tag in query:
|
||||
value = str(aInfo[tag]) # assure string
|
||||
if tag.endswith('matches'):
|
||||
# That one needs to be escaped since its content is
|
||||
# out of our control
|
||||
value = cls.escapeTag(value)
|
||||
string = string.replace('<' + tag + '>', value)
|
||||
# New line
|
||||
string = string.replace("<br>", '\n')
|
||||
return string
|
||||
|
||||
def _processCmd(self, cmd, aInfo = None):
|
||||
"""Executes a command with preliminary checks and substitutions.
|
||||
|
||||
Before executing any commands, executes the "check" command first
|
||||
in order to check if pre-requirements are met. If this check fails,
|
||||
it tries to restore a sane environment before executing the real
|
||||
command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cmd : str
|
||||
The command to execute.
|
||||
aInfo : dictionary
|
||||
Dynamic properties.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the command succeeded.
|
||||
"""
|
||||
if cmd == "":
|
||||
self._logSys.debug("Nothing to do")
|
||||
return True
|
||||
|
||||
checkCmd = self.replaceTag(self.actioncheck, self._properties)
|
||||
if not self.executeCmd(checkCmd, self.timeout):
|
||||
self._logSys.error(
|
||||
"Invariant check failed. Trying to restore a sane environment")
|
||||
self.stop()
|
||||
self.start()
|
||||
if not self.executeCmd(checkCmd, self.timeout):
|
||||
self._logSys.critical("Unable to restore environment")
|
||||
return False
|
||||
|
||||
# Replace tags
|
||||
if not aInfo is None:
|
||||
realCmd = self.replaceTag(cmd, aInfo)
|
||||
else:
|
||||
realCmd = cmd
|
||||
|
||||
# Replace static fields
|
||||
realCmd = self.replaceTag(realCmd, self._properties)
|
||||
|
||||
return self.executeCmd(realCmd, self.timeout)
|
||||
|
||||
@staticmethod
|
||||
def executeCmd(realCmd, timeout=60):
|
||||
"""Executes a command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
realCmd : str
|
||||
The command to execute.
|
||||
timeout : int
|
||||
The time out in seconds for the command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the command succeeded.
|
||||
|
||||
Raises
|
||||
------
|
||||
OSError
|
||||
If command fails to be executed.
|
||||
RuntimeError
|
||||
If command execution times out.
|
||||
"""
|
||||
logSys.debug(realCmd)
|
||||
if not realCmd:
|
||||
logSys.debug("Nothing to do")
|
||||
return True
|
||||
|
||||
_cmd_lock.acquire()
|
||||
try: # Try wrapped within another try needed for python version < 2.5
|
||||
stdout = tempfile.TemporaryFile(suffix=".stdout", prefix="fai2ban_")
|
||||
stderr = tempfile.TemporaryFile(suffix=".stderr", prefix="fai2ban_")
|
||||
try:
|
||||
popen = subprocess.Popen(
|
||||
realCmd, stdout=stdout, stderr=stderr, shell=True)
|
||||
stime = time.time()
|
||||
retcode = popen.poll()
|
||||
while time.time() - stime <= timeout and retcode is None:
|
||||
time.sleep(0.1)
|
||||
retcode = popen.poll()
|
||||
if retcode is None:
|
||||
logSys.error("%s -- timed out after %i seconds." %
|
||||
(realCmd, timeout))
|
||||
os.kill(popen.pid, signal.SIGTERM) # Terminate the process
|
||||
time.sleep(0.1)
|
||||
retcode = popen.poll()
|
||||
if retcode is None: # Still going...
|
||||
os.kill(popen.pid, signal.SIGKILL) # Kill the process
|
||||
time.sleep(0.1)
|
||||
retcode = popen.poll()
|
||||
except OSError, e:
|
||||
logSys.error("%s -- failed with %s" % (realCmd, e))
|
||||
finally:
|
||||
_cmd_lock.release()
|
||||
|
||||
std_level = retcode == 0 and logging.DEBUG or logging.ERROR
|
||||
if std_level >= logSys.getEffectiveLevel():
|
||||
stdout.seek(0)
|
||||
logSys.log(std_level, "%s -- stdout: %r" % (realCmd, stdout.read()))
|
||||
stderr.seek(0)
|
||||
logSys.log(std_level, "%s -- stderr: %r" % (realCmd, stderr.read()))
|
||||
stdout.close()
|
||||
stderr.close()
|
||||
|
||||
if retcode == 0:
|
||||
logSys.debug("%s -- returned successfully" % realCmd)
|
||||
return True
|
||||
elif retcode is None:
|
||||
logSys.error("%s -- unable to kill PID %i" % (realCmd, popen.pid))
|
||||
elif retcode < 0:
|
||||
logSys.error("%s -- killed with %s" %
|
||||
(realCmd, signame.get(-retcode, "signal %i" % -retcode)))
|
||||
else:
|
||||
msg = _RETCODE_HINTS.get(retcode, None)
|
||||
logSys.error("%s -- returned %i" % (realCmd, retcode))
|
||||
if msg:
|
||||
logSys.info("HINT on %i: %s"
|
||||
% (retcode, msg % locals()))
|
||||
return False
|
||||
raise RuntimeError("Command execution failed: %s" % realCmd)
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import time, logging
|
||||
import os
|
||||
import sys
|
||||
if sys.version_info >= (3, 3):
|
||||
import importlib.machinery
|
||||
else:
|
||||
import imp
|
||||
from collections import Mapping
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
OrderedDict = None
|
||||
|
||||
from .banmanager import BanManager
|
||||
from .jailthread import JailThread
|
||||
from .action import ActionBase, CommandAction, CallingMap
|
||||
from .mytime import MyTime
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class Actions(JailThread, Mapping):
|
||||
"""Handles jail actions.
|
||||
|
||||
This class handles the actions of the jail. Creation, deletion or to
|
||||
actions must be done through this class. This class is based on the
|
||||
Mapping type, and the `add` method must be used to add new actions.
|
||||
This class also starts and stops the actions, and fetches bans from
|
||||
the jail executing these bans via the actions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail: Jail
|
||||
The jail of which the actions belongs to.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
daemon
|
||||
ident
|
||||
name
|
||||
status
|
||||
active : bool
|
||||
Control the state of the thread.
|
||||
idle : bool
|
||||
Control the idle state of the thread.
|
||||
sleeptime : int
|
||||
The time the thread sleeps for in the loop.
|
||||
"""
|
||||
|
||||
def __init__(self, jail):
|
||||
JailThread.__init__(self)
|
||||
## The jail which contains this action.
|
||||
self._jail = jail
|
||||
if OrderedDict is not None:
|
||||
self._actions = OrderedDict()
|
||||
else:
|
||||
self._actions = dict()
|
||||
## The ban manager.
|
||||
self.__banManager = BanManager()
|
||||
|
||||
def add(self, name, pythonModule=None, initOpts=None):
|
||||
"""Adds a new action.
|
||||
|
||||
Add a new action if not already present, defaulting to standard
|
||||
`CommandAction`, or specified Python module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of the action.
|
||||
pythonModule : str, optional
|
||||
Path to Python file which must contain `Action` class.
|
||||
Default None, which means `CommandAction` is used.
|
||||
initOpts : dict, optional
|
||||
Options for Python Action, used as keyword arguments for
|
||||
initialisation. Default None.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If action name already exists.
|
||||
RuntimeError
|
||||
If external Python module does not have `Action` class
|
||||
or does not implement necessary methods as per `ActionBase`
|
||||
abstract class.
|
||||
"""
|
||||
# Check is action name already exists
|
||||
if name in self._actions:
|
||||
raise ValueError("Action %s already exists" % name)
|
||||
if pythonModule is None:
|
||||
action = CommandAction(self._jail, name)
|
||||
else:
|
||||
pythonModuleName = os.path.splitext(
|
||||
os.path.basename(pythonModule))[0]
|
||||
if sys.version_info >= (3, 3):
|
||||
customActionModule = importlib.machinery.SourceFileLoader(
|
||||
pythonModuleName, pythonModule).load_module()
|
||||
else:
|
||||
customActionModule = imp.load_source(
|
||||
pythonModuleName, pythonModule)
|
||||
if not hasattr(customActionModule, "Action"):
|
||||
raise RuntimeError(
|
||||
"%s module does not have 'Action' class" % pythonModule)
|
||||
elif not issubclass(customActionModule.Action, ActionBase):
|
||||
raise RuntimeError(
|
||||
"%s module %s does not implement required methods" % (
|
||||
pythonModule, customActionModule.Action.__name__))
|
||||
action = customActionModule.Action(self._jail, name, **initOpts)
|
||||
self._actions[name] = action
|
||||
|
||||
def __getitem__(self, name):
|
||||
try:
|
||||
return self._actions[name]
|
||||
except KeyError:
|
||||
raise KeyError("Invalid Action name: %s" % name)
|
||||
|
||||
def __delitem__(self, name):
|
||||
try:
|
||||
del self._actions[name]
|
||||
except KeyError:
|
||||
raise KeyError("Invalid Action name: %s" % name)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._actions)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._actions)
|
||||
|
||||
def __eq__(self, other): # Required for Threading
|
||||
return False
|
||||
|
||||
def __hash__(self): # Required for Threading
|
||||
return id(self)
|
||||
|
||||
##
|
||||
# Set the ban time.
|
||||
#
|
||||
# @param value the time
|
||||
|
||||
def setBanTime(self, value):
|
||||
self.__banManager.setBanTime(value)
|
||||
logSys.info("Set banTime = %s" % value)
|
||||
|
||||
##
|
||||
# Get the ban time.
|
||||
#
|
||||
# @return the time
|
||||
|
||||
def getBanTime(self):
|
||||
return self.__banManager.getBanTime()
|
||||
|
||||
def removeBannedIP(self, ip):
|
||||
"""Removes banned IP calling actions' unban method
|
||||
|
||||
Remove a banned IP now, rather than waiting for it to expire,
|
||||
even if set to never expire.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ip : str
|
||||
The IP address to unban
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If `ip` is not banned
|
||||
"""
|
||||
# Find the ticket with the IP.
|
||||
ticket = self.__banManager.getTicketByIP(ip)
|
||||
if ticket is not None:
|
||||
# Unban the IP.
|
||||
self.__unBan(ticket)
|
||||
else:
|
||||
raise ValueError("IP %s is not banned" % ip)
|
||||
|
||||
def run(self):
|
||||
"""Main loop for Threading.
|
||||
|
||||
This function is the main loop of the thread. It checks the jail
|
||||
queue and executes commands when an IP address is banned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the thread exits nicely.
|
||||
"""
|
||||
for name, action in self._actions.iteritems():
|
||||
try:
|
||||
action.start()
|
||||
except Exception as e:
|
||||
logSys.error("Failed to start jail '%s' action '%s': %s",
|
||||
self._jail.name, name, e)
|
||||
while self.active:
|
||||
if not self.idle:
|
||||
#logSys.debug(self._jail.name + ": action")
|
||||
ret = self.__checkBan()
|
||||
if not ret:
|
||||
self.__checkUnBan()
|
||||
time.sleep(self.sleeptime)
|
||||
else:
|
||||
time.sleep(self.sleeptime)
|
||||
self.__flushBan()
|
||||
|
||||
actions = self._actions.items()
|
||||
actions.reverse()
|
||||
for name, action in actions:
|
||||
try:
|
||||
action.stop()
|
||||
except Exception as e:
|
||||
logSys.error("Failed to stop jail '%s' action '%s': %s",
|
||||
self._jail.name, name, e)
|
||||
logSys.debug(self._jail.name + ": action terminated")
|
||||
return True
|
||||
|
||||
def __checkBan(self):
|
||||
"""Check for IP address to ban.
|
||||
|
||||
Look in the jail queue for FailTicket. If a ticket is available,
|
||||
it executes the "ban" command and adds a ticket to the BanManager.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if an IP address get banned.
|
||||
"""
|
||||
ticket = self._jail.getFailTicket()
|
||||
if ticket != False:
|
||||
aInfo = CallingMap()
|
||||
bTicket = BanManager.createBanTicket(ticket)
|
||||
aInfo["ip"] = bTicket.getIP()
|
||||
aInfo["failures"] = bTicket.getAttempt()
|
||||
aInfo["time"] = bTicket.getTime()
|
||||
aInfo["matches"] = "\n".join(bTicket.getMatches())
|
||||
if self._jail.database is not None:
|
||||
aInfo["ipmatches"] = lambda: "\n".join(
|
||||
self._jail.database.getBansMerged(
|
||||
ip=bTicket.getIP()).getMatches())
|
||||
aInfo["ipjailmatches"] = lambda: "\n".join(
|
||||
self._jail.database.getBansMerged(
|
||||
ip=bTicket.getIP(), jail=self._jail).getMatches())
|
||||
aInfo["ipfailures"] = lambda: "\n".join(
|
||||
self._jail.database.getBansMerged(
|
||||
ip=bTicket.getIP()).getAttempt())
|
||||
aInfo["ipjailfailures"] = lambda: "\n".join(
|
||||
self._jail.database.getBansMerged(
|
||||
ip=bTicket.getIP(), jail=self._jail).getAttempt())
|
||||
if self.__banManager.addBanTicket(bTicket):
|
||||
logSys.notice("[%s] Ban %s" % (self._jail.name, aInfo["ip"]))
|
||||
for name, action in self._actions.iteritems():
|
||||
try:
|
||||
action.ban(aInfo)
|
||||
except Exception as e:
|
||||
logSys.error(
|
||||
"Failed to execute ban jail '%s' action '%s': %s",
|
||||
self._jail.name, name, e)
|
||||
return True
|
||||
else:
|
||||
logSys.notice("[%s] %s already banned" % (self._jail.name,
|
||||
aInfo["ip"]))
|
||||
return False
|
||||
|
||||
def __checkUnBan(self):
|
||||
"""Check for IP address to unban.
|
||||
|
||||
Unban IP addresses which are outdated.
|
||||
"""
|
||||
for ticket in self.__banManager.unBanList(MyTime.time()):
|
||||
self.__unBan(ticket)
|
||||
|
||||
def __flushBan(self):
|
||||
"""Flush the ban list.
|
||||
|
||||
Unban all IP address which are still in the banning list.
|
||||
"""
|
||||
logSys.debug("Flush ban list")
|
||||
for ticket in self.__banManager.flushBanList():
|
||||
self.__unBan(ticket)
|
||||
|
||||
def __unBan(self, ticket):
|
||||
"""Unbans host corresponding to the ticket.
|
||||
|
||||
Executes the actions in order to unban the host given in the
|
||||
ticket.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ticket : FailTicket
|
||||
Ticket of failures of which to unban
|
||||
"""
|
||||
aInfo = dict()
|
||||
aInfo["ip"] = ticket.getIP()
|
||||
aInfo["failures"] = ticket.getAttempt()
|
||||
aInfo["time"] = ticket.getTime()
|
||||
aInfo["matches"] = "".join(ticket.getMatches())
|
||||
logSys.notice("[%s] Unban %s" % (self._jail.name, aInfo["ip"]))
|
||||
for name, action in self._actions.iteritems():
|
||||
try:
|
||||
action.unban(aInfo)
|
||||
except Exception as e:
|
||||
logSys.error(
|
||||
"Failed to execute unban jail '%s' action '%s': %s",
|
||||
self._jail.name, name, e)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Status of active bans, and total ban counts.
|
||||
"""
|
||||
ret = [("Currently banned", self.__banManager.size()),
|
||||
("Total banned", self.__banManager.getBanTotal()),
|
||||
("Banned IP list", self.__banManager.getBanList())]
|
||||
return ret
|
|
@ -25,11 +25,19 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
|||
__license__ = "GPL"
|
||||
|
||||
from pickle import dumps, loads, HIGHEST_PROTOCOL
|
||||
from common import helpers
|
||||
import asyncore, asynchat, socket, os, logging, sys, traceback, fcntl
|
||||
|
||||
from .. import helpers
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.server")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
if sys.version_info >= (3,):
|
||||
# b"" causes SyntaxError in python <= 2.5, so below implements equivalent
|
||||
EMPTY_BYTES = bytes("", encoding="ascii")
|
||||
else:
|
||||
# python 2.x, string type is equivalent to bytes.
|
||||
EMPTY_BYTES = ""
|
||||
|
||||
##
|
||||
# Request handler class.
|
||||
|
@ -39,7 +47,10 @@ logSys = logging.getLogger("fail2ban.server")
|
|||
|
||||
class RequestHandler(asynchat.async_chat):
|
||||
|
||||
END_STRING = "<F2B_END_COMMAND>"
|
||||
if sys.version_info >= (3,):
|
||||
END_STRING = bytes("<F2B_END_COMMAND>", encoding="ascii")
|
||||
else:
|
||||
END_STRING = "<F2B_END_COMMAND>"
|
||||
|
||||
def __init__(self, conn, transmitter):
|
||||
asynchat.async_chat.__init__(self, conn)
|
||||
|
@ -59,7 +70,7 @@ class RequestHandler(asynchat.async_chat):
|
|||
|
||||
def found_terminator(self):
|
||||
# Joins the buffer items.
|
||||
message = loads("".join(self.__buffer))
|
||||
message = loads(EMPTY_BYTES.join(self.__buffer))
|
||||
# Gives the message to the transmitter.
|
||||
message = self.__transmitter.proceed(message)
|
||||
# Serializes the response.
|
||||
|
@ -121,7 +132,7 @@ class AsyncServer(asyncore.dispatcher):
|
|||
if os.path.exists(sock):
|
||||
logSys.error("Fail2ban seems to be already running")
|
||||
if force:
|
||||
logSys.warn("Forcing execution of the server")
|
||||
logSys.warning("Forcing execution of the server")
|
||||
os.remove(sock)
|
||||
else:
|
||||
raise AsyncServerException("Server already running")
|
|
@ -24,13 +24,14 @@ __author__ = "Cyril Jaquier"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
from ticket import BanTicket
|
||||
from threading import Lock
|
||||
from mytime import MyTime
|
||||
import logging
|
||||
from threading import Lock
|
||||
|
||||
from .ticket import BanTicket
|
||||
from .mytime import MyTime
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.action")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
##
|
||||
# Banning Manager.
|
|
@ -0,0 +1,461 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Steven Hiscocks"
|
||||
__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import shutil, time
|
||||
import sqlite3
|
||||
import json
|
||||
import locale
|
||||
from functools import wraps
|
||||
from threading import Lock
|
||||
|
||||
from .mytime import MyTime
|
||||
from .ticket import FailTicket
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
if sys.version_info >= (3,):
|
||||
sqlite3.register_adapter(
|
||||
dict,
|
||||
lambda x: json.dumps(x, ensure_ascii=False).encode(
|
||||
locale.getpreferredencoding(), 'replace'))
|
||||
sqlite3.register_converter(
|
||||
"JSON",
|
||||
lambda x: json.loads(x.decode(
|
||||
locale.getpreferredencoding(), 'replace')))
|
||||
else:
|
||||
sqlite3.register_adapter(dict, json.dumps)
|
||||
sqlite3.register_converter("JSON", json.loads)
|
||||
|
||||
def commitandrollback(f):
|
||||
@wraps(f)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
with self._lock: # Threading lock
|
||||
with self._db: # Auto commit and rollback on exception
|
||||
return f(self, self._db.cursor(), *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
class Fail2BanDb(object):
|
||||
"""Fail2Ban database for storing persistent data.
|
||||
|
||||
This allows after Fail2Ban is restarted to reinstated bans and
|
||||
to continue monitoring logs from the same point.
|
||||
|
||||
This will either create a new Fail2Ban database, connect to an
|
||||
existing, and if applicable upgrade the schema in the process.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : str
|
||||
File name for SQLite3 database, which will be created if
|
||||
doesn't already exist.
|
||||
purgeAge : int
|
||||
Purge age in seconds, used to remove old bans from
|
||||
database during purge.
|
||||
|
||||
Raises
|
||||
------
|
||||
sqlite3.OperationalError
|
||||
Error connecting/creating a SQLite3 database.
|
||||
RuntimeError
|
||||
If exisiting database fails to update to new schema.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
filename
|
||||
purgeage
|
||||
"""
|
||||
__version__ = 2
|
||||
# Note all _TABLE_* strings must end in ';' for py26 compatibility
|
||||
_TABLE_fail2banDb = "CREATE TABLE fail2banDb(version INTEGER);"
|
||||
_TABLE_jails = "CREATE TABLE jails(" \
|
||||
"name TEXT NOT NULL UNIQUE, " \
|
||||
"enabled INTEGER NOT NULL DEFAULT 1" \
|
||||
");" \
|
||||
"CREATE INDEX jails_name ON jails(name);"
|
||||
_TABLE_logs = "CREATE TABLE logs(" \
|
||||
"jail TEXT NOT NULL, " \
|
||||
"path TEXT, " \
|
||||
"firstlinemd5 TEXT, " \
|
||||
"lastfilepos INTEGER DEFAULT 0, " \
|
||||
"FOREIGN KEY(jail) REFERENCES jails(name) ON DELETE CASCADE, " \
|
||||
"UNIQUE(jail, path)," \
|
||||
"UNIQUE(jail, path, firstlinemd5)" \
|
||||
");" \
|
||||
"CREATE INDEX logs_path ON logs(path);" \
|
||||
"CREATE INDEX logs_jail_path ON logs(jail, path);"
|
||||
#TODO: systemd journal features \
|
||||
#"journalmatch TEXT, " \
|
||||
#"journlcursor TEXT, " \
|
||||
#"lastfiletime INTEGER DEFAULT 0, " # is this easily available \
|
||||
_TABLE_bans = "CREATE TABLE bans(" \
|
||||
"jail TEXT NOT NULL, " \
|
||||
"ip TEXT, " \
|
||||
"timeofban INTEGER NOT NULL, " \
|
||||
"data JSON, " \
|
||||
"FOREIGN KEY(jail) REFERENCES jails(name) " \
|
||||
");" \
|
||||
"CREATE INDEX bans_jail_timeofban_ip ON bans(jail, timeofban);" \
|
||||
"CREATE INDEX bans_jail_ip ON bans(jail, ip);" \
|
||||
"CREATE INDEX bans_ip ON bans(ip);" \
|
||||
|
||||
def __init__(self, filename, purgeAge=24*60*60):
|
||||
try:
|
||||
self._lock = Lock()
|
||||
self._db = sqlite3.connect(
|
||||
filename, check_same_thread=False,
|
||||
detect_types=sqlite3.PARSE_DECLTYPES)
|
||||
self._dbFilename = filename
|
||||
self._purgeAge = purgeAge
|
||||
|
||||
self._bansMergedCache = {}
|
||||
|
||||
logSys.info(
|
||||
"Connected to fail2ban persistent database '%s'", filename)
|
||||
except sqlite3.OperationalError, e:
|
||||
logSys.error(
|
||||
"Error connecting to fail2ban persistent database '%s': %s",
|
||||
filename, e.args[0])
|
||||
raise
|
||||
|
||||
cur = self._db.cursor()
|
||||
cur.execute("PRAGMA foreign_keys = ON;")
|
||||
|
||||
try:
|
||||
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
|
||||
except sqlite3.OperationalError:
|
||||
logSys.warning("New database created. Version '%i'",
|
||||
self.createDb())
|
||||
else:
|
||||
version = cur.fetchone()[0]
|
||||
if version < Fail2BanDb.__version__:
|
||||
newversion = self.updateDb(version)
|
||||
if newversion == Fail2BanDb.__version__:
|
||||
logSys.warning( "Database updated from '%i' to '%i'",
|
||||
version, newversion)
|
||||
else:
|
||||
logSys.error( "Database update failed to acheive version '%i'"
|
||||
": updated from '%i' to '%i'",
|
||||
Fail2BanDb.__version__, version, newversion)
|
||||
raise RuntimeError('Failed to fully update')
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
"""File name of SQLite3 database file.
|
||||
"""
|
||||
return self._dbFilename
|
||||
|
||||
@property
|
||||
def purgeage(self):
|
||||
"""Purge age in seconds.
|
||||
"""
|
||||
return self._purgeAge
|
||||
|
||||
@purgeage.setter
|
||||
def purgeage(self, value):
|
||||
self._purgeAge = int(value)
|
||||
|
||||
@commitandrollback
|
||||
def createDb(self, cur):
|
||||
"""Creates a new database, called during initialisation.
|
||||
"""
|
||||
# Version info
|
||||
cur.executescript(Fail2BanDb._TABLE_fail2banDb)
|
||||
cur.execute("INSERT INTO fail2banDb(version) VALUES(?)",
|
||||
(Fail2BanDb.__version__, ))
|
||||
# Jails
|
||||
cur.executescript(Fail2BanDb._TABLE_jails)
|
||||
# Logs
|
||||
cur.executescript(Fail2BanDb._TABLE_logs)
|
||||
# Bans
|
||||
cur.executescript(Fail2BanDb._TABLE_bans)
|
||||
|
||||
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
|
||||
return cur.fetchone()[0]
|
||||
|
||||
@commitandrollback
|
||||
def updateDb(self, cur, version):
|
||||
"""Update an existing database, called during initialisation.
|
||||
|
||||
A timestamped backup is also created prior to attempting the update.
|
||||
"""
|
||||
self._dbBackupFilename = self.filename + '.' + time.strftime('%Y%m%d-%H%M%S', MyTime.gmtime())
|
||||
shutil.copyfile(self.filename, self._dbBackupFilename)
|
||||
logSys.info("Database backup created: %s", self._dbBackupFilename)
|
||||
if version > Fail2BanDb.__version__:
|
||||
raise NotImplementedError(
|
||||
"Attempt to travel to future version of database ...how did you get here??")
|
||||
|
||||
if version < 2:
|
||||
cur.executescript("BEGIN TRANSACTION;"
|
||||
"CREATE TEMPORARY TABLE logs_temp AS SELECT * FROM logs;"
|
||||
"DROP TABLE logs;"
|
||||
"%s;"
|
||||
"INSERT INTO logs SELECT * from logs_temp;"
|
||||
"DROP TABLE logs_temp;"
|
||||
"UPDATE fail2banDb SET version = 2;"
|
||||
"COMMIT;" % Fail2BanDb._TABLE_logs)
|
||||
|
||||
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
|
||||
return cur.fetchone()[0]
|
||||
|
||||
@commitandrollback
|
||||
def addJail(self, cur, jail):
|
||||
"""Adds a jail to the database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
Jail to be added to the database.
|
||||
"""
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO jails(name, enabled) VALUES(?, 1)",
|
||||
(jail.name,))
|
||||
|
||||
@commitandrollback
|
||||
def delJail(self, cur, jail):
|
||||
"""Deletes a jail from the database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
Jail to be removed from the database.
|
||||
"""
|
||||
# Will be deleted by purge as appropriate
|
||||
cur.execute(
|
||||
"UPDATE jails SET enabled=0 WHERE name=?", (jail.name, ))
|
||||
|
||||
@commitandrollback
|
||||
def delAllJails(self, cur):
|
||||
"""Deletes all jails from the database.
|
||||
"""
|
||||
# Will be deleted by purge as appropriate
|
||||
cur.execute("UPDATE jails SET enabled=0")
|
||||
|
||||
@commitandrollback
|
||||
def getJailNames(self, cur):
|
||||
"""Get name of jails in database.
|
||||
|
||||
Currently only used for testing purposes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of jail names.
|
||||
"""
|
||||
cur.execute("SELECT name FROM jails")
|
||||
return set(row[0] for row in cur.fetchmany())
|
||||
|
||||
@commitandrollback
|
||||
def addLog(self, cur, jail, container):
|
||||
"""Adds a log to the database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
Jail that log is being monitored by.
|
||||
container : FileContainer
|
||||
File container of the log file being added.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
If log was already present in database, value of last position
|
||||
in the log file; else `None`
|
||||
"""
|
||||
lastLinePos = None
|
||||
cur.execute(
|
||||
"SELECT firstlinemd5, lastfilepos FROM logs "
|
||||
"WHERE jail=? AND path=?",
|
||||
(jail.name, container.getFileName()))
|
||||
try:
|
||||
firstLineMD5, lastLinePos = cur.fetchone()
|
||||
except TypeError:
|
||||
firstLineMD5 = False
|
||||
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO logs(jail, path, firstlinemd5, lastfilepos) "
|
||||
"VALUES(?, ?, ?, ?)",
|
||||
(jail.name, container.getFileName(),
|
||||
container.getHash(), container.getPos()))
|
||||
if container.getHash() != firstLineMD5:
|
||||
lastLinePos = None
|
||||
return lastLinePos
|
||||
|
||||
@commitandrollback
|
||||
def getLogPaths(self, cur, jail=None):
|
||||
"""Gets all the log paths from the database.
|
||||
|
||||
Currently only for testing purposes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
If specified, will only reutrn logs belonging to the jail.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of log paths.
|
||||
"""
|
||||
query = "SELECT path FROM logs"
|
||||
queryArgs = []
|
||||
if jail is not None:
|
||||
query += " WHERE jail=?"
|
||||
queryArgs.append(jail.name)
|
||||
cur.execute(query, queryArgs)
|
||||
return set(row[0] for row in cur.fetchmany())
|
||||
|
||||
@commitandrollback
|
||||
def updateLog(self, cur, *args, **kwargs):
|
||||
"""Updates hash and last position in log file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
Jail of which the log file belongs to.
|
||||
container : FileContainer
|
||||
File container of the log file being updated.
|
||||
"""
|
||||
self._updateLog(cur, *args, **kwargs)
|
||||
|
||||
def _updateLog(self, cur, jail, container):
|
||||
cur.execute(
|
||||
"UPDATE logs SET firstlinemd5=?, lastfilepos=? "
|
||||
"WHERE jail=? AND path=?",
|
||||
(container.getHash(), container.getPos(),
|
||||
jail.name, container.getFileName()))
|
||||
|
||||
@commitandrollback
|
||||
def addBan(self, cur, jail, ticket):
|
||||
"""Add a ban to the database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
Jail in which the ban has occured.
|
||||
ticket : BanTicket
|
||||
Ticket of the ban to be added.
|
||||
"""
|
||||
self._bansMergedCache = {}
|
||||
#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(),
|
||||
{"matches": ticket.getMatches(),
|
||||
"failures": ticket.getAttempt()}))
|
||||
|
||||
@commitandrollback
|
||||
def _getBans(self, cur, jail=None, bantime=None, ip=None):
|
||||
query = "SELECT ip, timeofban, data FROM bans WHERE 1"
|
||||
queryArgs = []
|
||||
|
||||
if jail is not None:
|
||||
query += " AND jail=?"
|
||||
queryArgs.append(jail.name)
|
||||
if bantime is not None:
|
||||
query += " AND timeofban > ?"
|
||||
queryArgs.append(MyTime.time() - bantime)
|
||||
if ip is not None:
|
||||
query += " AND ip=?"
|
||||
queryArgs.append(ip)
|
||||
query += " ORDER BY timeofban"
|
||||
|
||||
return cur.execute(query, queryArgs)
|
||||
|
||||
def getBans(self, **kwargs):
|
||||
"""Get bans from the database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
Jail that the ban belongs to. Default `None`; all jails.
|
||||
bantime : int
|
||||
Ban time in seconds, such that bans returned would still be
|
||||
valid now. Default `None`; no limit.
|
||||
ip : str
|
||||
IP Address to filter bans by. Default `None`; all IPs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
List of `Ticket`s for bans stored in database.
|
||||
"""
|
||||
tickets = []
|
||||
for ip, timeofban, data in self._getBans(**kwargs):
|
||||
#TODO: Implement data parts once arbitrary match keys completed
|
||||
tickets.append(FailTicket(ip, timeofban, data['matches']))
|
||||
tickets[-1].setAttempt(data['failures'])
|
||||
return tickets
|
||||
|
||||
def getBansMerged(self, ip, jail=None, **kwargs):
|
||||
"""Get bans from the database, merged into single ticket.
|
||||
|
||||
This is the same as `getBans`, but bans merged into single
|
||||
ticket.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
Jail that the ban belongs to. Default `None`; all jails.
|
||||
bantime : int
|
||||
Ban time in seconds, such that bans returned would still be
|
||||
valid now. Default `None`; no limit.
|
||||
ip : str
|
||||
IP Address to filter bans by. Default `None`; all IPs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Ticket
|
||||
Single ticket representing bans stored in database.
|
||||
"""
|
||||
cacheKey = ip if jail is None else "%s|%s" % (ip, jail.name)
|
||||
if cacheKey in self._bansMergedCache:
|
||||
return self._bansMergedCache[cacheKey]
|
||||
matches = []
|
||||
failures = 0
|
||||
for ip, timeofban, data in self._getBans(ip=ip, jail=jail, **kwargs):
|
||||
#TODO: Implement data parts once arbitrary match keys completed
|
||||
matches.extend(data['matches'])
|
||||
failures += data['failures']
|
||||
ticket = FailTicket(ip, timeofban, matches)
|
||||
ticket.setAttempt(failures)
|
||||
self._bansMergedCache[cacheKey] = ticket
|
||||
return ticket
|
||||
|
||||
@commitandrollback
|
||||
def purge(self, cur):
|
||||
"""Purge old bans, jails and log files from database.
|
||||
"""
|
||||
self._bansMergedCache = {}
|
||||
cur.execute(
|
||||
"DELETE FROM bans WHERE timeofban < ?",
|
||||
(MyTime.time() - self._purgeAge, ))
|
||||
cur.execute(
|
||||
"DELETE FROM jails WHERE enabled = 0 "
|
||||
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name)")
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Cyril Jaquier and Fail2Ban Contributors"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import sys, time, logging
|
||||
from threading import Lock
|
||||
|
||||
from .datetemplate import DatePatternRegex, DateTai64n, DateEpoch
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class DateDetector(object):
|
||||
"""Manages one or more date templates to find a date within a log line.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
templates
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.__lock = Lock()
|
||||
self.__templates = list()
|
||||
self.__known_names = set()
|
||||
|
||||
def _appendTemplate(self, template):
|
||||
name = template.name
|
||||
if name in self.__known_names:
|
||||
raise ValueError(
|
||||
"There is already a template with name %s" % name)
|
||||
self.__known_names.add(name)
|
||||
self.__templates.append(template)
|
||||
|
||||
def appendTemplate(self, template):
|
||||
"""Add a date template to manage and use in search of dates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
template : DateTemplate or str
|
||||
Can be either a `DateTemplate` instance, or a string which will
|
||||
be used as the pattern for the `DatePatternRegex` template. The
|
||||
template will then be added to the detector.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If a template already exists with the same name.
|
||||
"""
|
||||
if isinstance(template, str):
|
||||
template = DatePatternRegex(template)
|
||||
self._appendTemplate(template)
|
||||
|
||||
def addDefaultTemplate(self):
|
||||
"""Add Fail2Ban's default set of date templates.
|
||||
"""
|
||||
self.__lock.acquire()
|
||||
try:
|
||||
# asctime with optional day, subsecond and/or year:
|
||||
# Sun Jan 23 21:59:59.011 2005
|
||||
self.appendTemplate("(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %Y)?")
|
||||
# simple date, optional subsecond (proftpd):
|
||||
# 2005-01-23 21:59:59
|
||||
# simple date: 2005/01/23 21:59:59
|
||||
# custom for syslog-ng 2006.12.21 06:43:20
|
||||
self.appendTemplate("%Y(?P<_sep>[-/.])%m(?P=_sep)%d %H:%M:%S(?:,%f)?")
|
||||
# simple date too (from x11vnc): 23/01/2005 21:59:59
|
||||
# and with optional year given by 2 digits: 23/01/05 21:59:59
|
||||
# (See http://bugs.debian.org/537610)
|
||||
# 17-07-2008 17:23:25
|
||||
self.appendTemplate("%d(?P<_sep>[-/])%m(?P=_sep)(?:%Y|%y) %H:%M:%S")
|
||||
# Apache format optional time zone:
|
||||
# [31/Oct/2006:09:22:55 -0000]
|
||||
# 26-Jul-2007 15:20:52
|
||||
self.appendTemplate("%d(?P<_sep>[-/])%b(?P=_sep)%Y[ :]?%H:%M:%S(?:\.%f)?(?: %z)?")
|
||||
# CPanel 05/20/2008:01:57:39
|
||||
self.appendTemplate("%m/%d/%Y:%H:%M:%S")
|
||||
# named 26-Jul-2007 15:20:52.252
|
||||
# roundcube 26-Jul-2007 15:20:52 +0200
|
||||
# 01-27-2012 16:22:44.252
|
||||
# subseconds explicit to avoid possible %m<->%d confusion
|
||||
# with previous
|
||||
self.appendTemplate("%m-%d-%Y %H:%M:%S\.%f")
|
||||
# TAI64N
|
||||
template = DateTai64n()
|
||||
template.name = "TAI64N"
|
||||
self.appendTemplate(template)
|
||||
# Epoch
|
||||
template = DateEpoch()
|
||||
template.name = "Epoch"
|
||||
self.appendTemplate(template)
|
||||
# ISO 8601
|
||||
self.appendTemplate("%Y-%m-%d[T ]%H:%M:%S(?:\.%f)?(?:%z)?")
|
||||
# Only time information in the log
|
||||
self.appendTemplate("^%H:%M:%S")
|
||||
# <09/16/08@05:03:30>
|
||||
self.appendTemplate("^<%m/%d/%y@%H:%M:%S>")
|
||||
# MySQL: 130322 11:46:11
|
||||
self.appendTemplate("^%y%m%d ?%H:%M:%S")
|
||||
# Apache Tomcat
|
||||
self.appendTemplate("%b %d, %Y %I:%M:%S %p")
|
||||
# ASSP: Apr-27-13 02:33:06
|
||||
self.appendTemplate("^%b-%d-%y %H:%M:%S")
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
@property
|
||||
def templates(self):
|
||||
"""List of template instances managed by the detector.
|
||||
"""
|
||||
return self.__templates
|
||||
|
||||
def matchTime(self, line):
|
||||
"""Attempts to find date on a log line using templates.
|
||||
|
||||
This uses the templates' `matchDate` method in an attempt to find
|
||||
a date. It also increments the match hit count for the winning
|
||||
template.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Line which is searched by the date templates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
re.MatchObject
|
||||
The regex match returned from the first successfully matched
|
||||
template.
|
||||
"""
|
||||
self.__lock.acquire()
|
||||
try:
|
||||
for template in self.__templates:
|
||||
match = template.matchDate(line)
|
||||
if not match is None:
|
||||
logSys.debug("Matched time template %s" % template.name)
|
||||
template.hits += 1
|
||||
return match
|
||||
return None
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
def getTime(self, line):
|
||||
"""Attempts to return the date on a log line using templates.
|
||||
|
||||
This uses the templates' `getDate` method in an attempt to find
|
||||
a date.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Line which is searched by the date templates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Unix timestamp returned from the first successfully matched
|
||||
template.
|
||||
"""
|
||||
self.__lock.acquire()
|
||||
try:
|
||||
for template in self.__templates:
|
||||
try:
|
||||
date = template.getDate(line)
|
||||
if date is None:
|
||||
continue
|
||||
logSys.debug("Got time %f for \"%r\" using template %s" %
|
||||
(date[0], date[1].group(), template.name))
|
||||
return date
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
def sortTemplate(self):
|
||||
"""Sort the date templates by number of hits
|
||||
|
||||
Sort the template lists using the hits score. This method is not
|
||||
called in this object and thus should be called from time to time.
|
||||
This ensures the most commonly matched templates are checked first,
|
||||
improving performance of matchTime and getTime.
|
||||
"""
|
||||
self.__lock.acquire()
|
||||
try:
|
||||
logSys.debug("Sorting the template list")
|
||||
self.__templates.sort(key=lambda x: x.hits, reverse=True)
|
||||
t = self.__templates[0]
|
||||
logSys.debug("Winning template: %s with %d hits" % (t.name, t.hits))
|
||||
finally:
|
||||
self.__lock.release()
|
|
@ -0,0 +1,278 @@
|
|||
# emacs: -*- mode: python; coding: utf-8; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import re, time, calendar
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
from .mytime import MyTime
|
||||
from .strptime import reGroupDictStrptime, timeRE
|
||||
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DateTemplate(object):
|
||||
"""A template which searches for and returns a date from a log line.
|
||||
|
||||
This is an not functional abstract class which other templates should
|
||||
inherit from.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name
|
||||
regex
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._name = ""
|
||||
self._regex = ""
|
||||
self._cRegex = None
|
||||
self.hits = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name assigned to template.
|
||||
"""
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
self._name = name
|
||||
|
||||
def getRegex(self):
|
||||
return self._regex
|
||||
|
||||
def setRegex(self, regex, wordBegin=True):
|
||||
"""Sets regex to use for searching for date in log line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
regex : str
|
||||
The regex the template will use for searching for a date.
|
||||
wordBegin : bool
|
||||
Defines whether the regex should be modified to search at
|
||||
begining of a word, by adding "\\b" to start of regex.
|
||||
Default True.
|
||||
|
||||
Raises
|
||||
------
|
||||
re.error
|
||||
If regular expression fails to compile
|
||||
"""
|
||||
regex = regex.strip()
|
||||
if (wordBegin and not re.search(r'^\^', regex)):
|
||||
regex = r'\b' + regex
|
||||
self._regex = regex
|
||||
self._cRegex = re.compile(regex, re.UNICODE | re.IGNORECASE)
|
||||
|
||||
regex = property(getRegex, setRegex, doc=
|
||||
"""Regex used to search for date.
|
||||
""")
|
||||
|
||||
def matchDate(self, line):
|
||||
"""Check if regex for date matches on a log line.
|
||||
"""
|
||||
dateMatch = self._cRegex.search(line)
|
||||
return dateMatch
|
||||
|
||||
@abstractmethod
|
||||
def getDate(self, line):
|
||||
"""Abstract method, which should return the date for a log line
|
||||
|
||||
This should return the date for a log line, typically taking the
|
||||
date from the part of the line which matched the templates regex.
|
||||
This requires abstraction, therefore just raises exception.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Log line, of which the date should be extracted from.
|
||||
|
||||
Raises
|
||||
------
|
||||
NotImplementedError
|
||||
Abstract method, therefore always returns this.
|
||||
"""
|
||||
raise NotImplementedError("getDate() is abstract")
|
||||
|
||||
|
||||
class DateEpoch(DateTemplate):
|
||||
"""A date template which searches for Unix timestamps.
|
||||
|
||||
This includes Unix timestamps which appear at start of a line, optionally
|
||||
within square braces (nsd), or on SELinux audit log lines.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name
|
||||
regex
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
DateTemplate.__init__(self)
|
||||
self.regex = "(?:^|(?P<square>(?<=^\[))|(?P<selinux>(?<=audit\()))\d{10}(?:\.\d{3,6})?(?(selinux)(?=:\d+\))(?(square)(?=\])))"
|
||||
|
||||
def getDate(self, line):
|
||||
"""Method to return the date for a log line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Log line, of which the date should be extracted from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(float, str)
|
||||
Tuple containing a Unix timestamp, and the string of the date
|
||||
which was matched and in turned used to calculated the timestamp.
|
||||
"""
|
||||
dateMatch = self.matchDate(line)
|
||||
if dateMatch:
|
||||
# extract part of format which represents seconds since epoch
|
||||
return (float(dateMatch.group()), dateMatch)
|
||||
return None
|
||||
|
||||
class DatePatternRegex(DateTemplate):
|
||||
"""Date template, with regex/pattern
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pattern : str
|
||||
Sets the date templates pattern.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name
|
||||
regex
|
||||
pattern
|
||||
"""
|
||||
_patternRE = r"%%(%%|[%s])" % "".join(timeRE.keys())
|
||||
_patternName = {
|
||||
'a': "DAY", 'A': "DAYNAME", 'b': "MON", 'B': "MONTH", 'd': "Day",
|
||||
'H': "24hour", 'I': "12hour", 'j': "Yearday", 'm': "Month",
|
||||
'M': "Minute", 'p': "AMPM", 'S': "Second", 'U': "Yearweek",
|
||||
'w': "Weekday", 'W': "Yearweek", 'y': 'Year2', 'Y': "Year", '%': "%",
|
||||
'z': "Zone offset", 'f': "Microseconds", 'Z': "Zone name"}
|
||||
for _key in set(timeRE) - set(_patternName): # may not have them all...
|
||||
_patternName[_key] = "%%%s" % _key
|
||||
|
||||
def __init__(self, pattern=None):
|
||||
super(DatePatternRegex, self).__init__()
|
||||
self._pattern = None
|
||||
if pattern is not None:
|
||||
self.pattern = pattern
|
||||
|
||||
@property
|
||||
def pattern(self):
|
||||
"""The pattern used for regex with strptime "%" time fields.
|
||||
|
||||
This should be a valid regular expression, of which matching string
|
||||
will be extracted from the log line. strptime style "%" fields will
|
||||
be replaced by appropriate regular expressions, or custom regex
|
||||
groups with names as per the strptime fields can also be used
|
||||
instead.
|
||||
"""
|
||||
return self._pattern
|
||||
|
||||
@pattern.setter
|
||||
def pattern(self, pattern):
|
||||
self._pattern = pattern
|
||||
self._name = re.sub(
|
||||
self._patternRE, r'%(\1)s', pattern) % self._patternName
|
||||
super(DatePatternRegex, self).setRegex(
|
||||
re.sub(self._patternRE, r'%(\1)s', pattern) % timeRE)
|
||||
|
||||
def setRegex(self, value):
|
||||
raise NotImplementedError("Regex derived from pattern")
|
||||
|
||||
@DateTemplate.name.setter
|
||||
def name(self, value):
|
||||
raise NotImplementedError("Name derived from pattern")
|
||||
|
||||
def getDate(self, line):
|
||||
"""Method to return the date for a log line.
|
||||
|
||||
This uses a custom version of strptime, using the named groups
|
||||
from the instances `pattern` property.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Log line, of which the date should be extracted from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(float, str)
|
||||
Tuple containing a Unix timestamp, and the string of the date
|
||||
which was matched and in turned used to calculated the timestamp.
|
||||
"""
|
||||
dateMatch = self.matchDate(line)
|
||||
if dateMatch:
|
||||
groupdict = dict(
|
||||
(key, value)
|
||||
for key, value in dateMatch.groupdict().iteritems()
|
||||
if value is not None)
|
||||
return reGroupDictStrptime(groupdict), dateMatch
|
||||
|
||||
class DateTai64n(DateTemplate):
|
||||
"""A date template which matches TAI64N formate timestamps.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name
|
||||
regex
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
DateTemplate.__init__(self)
|
||||
# We already know the format for TAI64N
|
||||
# yoh: we should not add an additional front anchor
|
||||
self.setRegex("@[0-9a-f]{24}", wordBegin=False)
|
||||
|
||||
def getDate(self, line):
|
||||
"""Method to return the date for a log line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Log line, of which the date should be extracted from.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(float, str)
|
||||
Tuple containing a Unix timestamp, and the string of the date
|
||||
which was matched and in turned used to calculated the timestamp.
|
||||
"""
|
||||
dateMatch = self.matchDate(line)
|
||||
if dateMatch:
|
||||
# extract part of format which represents seconds since epoch
|
||||
value = dateMatch.group()
|
||||
seconds_since_epoch = value[2:17]
|
||||
# convert seconds from HEX into local time stamp
|
||||
return (int(seconds_since_epoch, 16), dateMatch)
|
||||
return None
|
|
@ -27,7 +27,7 @@ __license__ = "GPL"
|
|||
import logging
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class FailData:
|
||||
|
|
@ -24,13 +24,14 @@ __author__ = "Cyril Jaquier"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
from faildata import FailData
|
||||
from ticket import FailTicket
|
||||
from threading import Lock
|
||||
import logging
|
||||
|
||||
from .faildata import FailData
|
||||
from .ticket import FailTicket
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.filter")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class FailManager:
|
||||
|
|
@ -21,7 +21,7 @@ __author__ = "Cyril Jaquier"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import re, sre_constants
|
||||
import re, sre_constants, sys
|
||||
|
||||
##
|
||||
# Regular expression class.
|
||||
|
@ -42,10 +42,15 @@ class Regex:
|
|||
# Perform shortcuts expansions.
|
||||
# Replace "<HOST>" with default regular expression for host.
|
||||
regex = regex.replace("<HOST>", "(?:::f{4,6}:)?(?P<host>[\w\-.^_]*\w)")
|
||||
# Replace "<SKIPLINES>" with regular expression for multiple lines.
|
||||
regexSplit = regex.split("<SKIPLINES>")
|
||||
regex = regexSplit[0]
|
||||
for n, regexLine in enumerate(regexSplit[1:]):
|
||||
regex += "\n(?P<skiplines%i>(?:(.*\n)*?))" % n + regexLine
|
||||
if regex.lstrip() == '':
|
||||
raise RegexException("Cannot add empty regex")
|
||||
try:
|
||||
self._regexObj = re.compile(regex)
|
||||
self._regexObj = re.compile(regex, re.MULTILINE)
|
||||
self._regex = regex
|
||||
except sre_constants.error:
|
||||
raise RegexException("Unable to compile regular expression '%s'" %
|
||||
|
@ -67,12 +72,44 @@ class Regex:
|
|||
# Sets an internal cache (match object) in order to avoid searching for
|
||||
# the pattern again. This method must be called before calling any other
|
||||
# method of this object.
|
||||
# @param value the line
|
||||
# @param a list of tupples. The tupples are ( prematch, datematch, postdatematch )
|
||||
|
||||
def search(self, value):
|
||||
self._matchCache = self._regexObj.search(value)
|
||||
|
||||
##
|
||||
def search(self, tupleLines):
|
||||
self._matchCache = self._regexObj.search(
|
||||
"\n".join("".join(value[::2]) for value in tupleLines) + "\n")
|
||||
if self.hasMatched():
|
||||
# Find start of the first line where the match was found
|
||||
try:
|
||||
self._matchLineStart = self._matchCache.string.rindex(
|
||||
"\n", 0, self._matchCache.start() +1 ) + 1
|
||||
except ValueError:
|
||||
self._matchLineStart = 0
|
||||
# Find end of the last line where the match was found
|
||||
try:
|
||||
self._matchLineEnd = self._matchCache.string.index(
|
||||
"\n", self._matchCache.end() - 1) + 1
|
||||
except ValueError:
|
||||
self._matchLineEnd = len(self._matchCache.string)
|
||||
|
||||
|
||||
lineCount1 = self._matchCache.string.count(
|
||||
"\n", 0, self._matchLineStart)
|
||||
lineCount2 = self._matchCache.string.count(
|
||||
"\n", 0, self._matchLineEnd)
|
||||
self._matchedTupleLines = tupleLines[lineCount1:lineCount2]
|
||||
self._unmatchedTupleLines = tupleLines[:lineCount1]
|
||||
|
||||
n = 0
|
||||
for skippedLine in self.getSkippedLines():
|
||||
for m, matchedTupleLine in enumerate(
|
||||
self._matchedTupleLines[n:]):
|
||||
if "".join(matchedTupleLine[::2]) == skippedLine:
|
||||
self._unmatchedTupleLines.append(
|
||||
self._matchedTupleLines.pop(n+m))
|
||||
n += m
|
||||
break
|
||||
self._unmatchedTupleLines.extend(tupleLines[lineCount2:])
|
||||
|
||||
# Checks if the previous call to search() matched.
|
||||
#
|
||||
# @return True if a match was found, False otherwise
|
||||
|
@ -83,6 +120,67 @@ class Regex:
|
|||
else:
|
||||
return False
|
||||
|
||||
##
|
||||
# Returns skipped lines.
|
||||
#
|
||||
# This returns skipped lines captured by the <SKIPLINES> tag.
|
||||
# @return list of skipped lines
|
||||
|
||||
def getSkippedLines(self):
|
||||
if not self._matchCache:
|
||||
return []
|
||||
skippedLines = ""
|
||||
n = 0
|
||||
while True:
|
||||
try:
|
||||
if self._matchCache.group("skiplines%i" % n) is not None:
|
||||
skippedLines += self._matchCache.group("skiplines%i" % n)
|
||||
n += 1
|
||||
except IndexError:
|
||||
break
|
||||
# KeyError is because of PyPy issue1665 affecting pypy <= 2.2.1
|
||||
except KeyError:
|
||||
if 'PyPy' not in sys.version: # pragma: no cover - not sure this is even reachable
|
||||
raise
|
||||
break
|
||||
return skippedLines.splitlines(False)
|
||||
|
||||
##
|
||||
# Returns unmatched lines.
|
||||
#
|
||||
# This returns unmatched lines including captured by the <SKIPLINES> tag.
|
||||
# @return list of unmatched lines
|
||||
|
||||
def getUnmatchedTupleLines(self):
|
||||
if not self.hasMatched():
|
||||
return []
|
||||
else:
|
||||
return self._unmatchedTupleLines
|
||||
|
||||
def getUnmatchedLines(self):
|
||||
if not self.hasMatched():
|
||||
return []
|
||||
else:
|
||||
return ["".join(line) for line in self._unmatchedTupleLines]
|
||||
|
||||
##
|
||||
# Returns matched lines.
|
||||
#
|
||||
# This returns matched lines by excluding those captured
|
||||
# by the <SKIPLINES> tag.
|
||||
# @return list of matched lines
|
||||
|
||||
def getMatchedTupleLines(self):
|
||||
if not self.hasMatched():
|
||||
return []
|
||||
else:
|
||||
return self._matchedTupleLines
|
||||
|
||||
def getMatchedLines(self):
|
||||
if not self.hasMatched():
|
||||
return []
|
||||
else:
|
||||
return ["".join(line) for line in self._matchedTupleLines]
|
||||
|
||||
##
|
||||
# Exception dedicated to the class Regex.
|
|
@ -21,21 +21,19 @@ __author__ = "Cyril Jaquier and Fail2Ban Contributors"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
import sys
|
||||
import logging, re, os, fcntl, time, sys, locale, codecs
|
||||
|
||||
from failmanager import FailManagerEmpty
|
||||
from failmanager import FailManager
|
||||
from ticket import FailTicket
|
||||
from jailthread import JailThread
|
||||
from datedetector import DateDetector
|
||||
from mytime import MyTime
|
||||
from failregex import FailRegex, Regex, RegexException
|
||||
from action import Action
|
||||
|
||||
import logging, re, os, fcntl, time, shlex, subprocess
|
||||
from .failmanager import FailManagerEmpty, FailManager
|
||||
from .ticket import FailTicket
|
||||
from .jailthread import JailThread
|
||||
from .datedetector import DateDetector
|
||||
from .datetemplate import DatePatternRegex, DateEpoch, DateTai64n
|
||||
from .mytime import MyTime
|
||||
from .failregex import FailRegex, Regex, RegexException
|
||||
from .action import CommandAction
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.filter")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
##
|
||||
# Log reader class.
|
||||
|
@ -68,6 +66,13 @@ class Filter(JailThread):
|
|||
self.__findTime = 6000
|
||||
## The ignore IP list.
|
||||
self.__ignoreIpList = []
|
||||
## Size of line buffer
|
||||
self.__lineBufferSize = 1
|
||||
## Line buffer
|
||||
self.__lineBuffer = []
|
||||
## Store last time stamp, applicable for multi-line
|
||||
self.__lastTimeText = ""
|
||||
self.__lastDate = None
|
||||
## External command
|
||||
self.__ignoreCommand = False
|
||||
|
||||
|
@ -90,6 +95,10 @@ class Filter(JailThread):
|
|||
try:
|
||||
regex = FailRegex(value)
|
||||
self.__failRegex.append(regex)
|
||||
if "\n" in regex.getRegex() and not self.getMaxLines() > 1:
|
||||
logSys.warning(
|
||||
"Mutliline regex set for jail '%s' "
|
||||
"but maxlines not greater than 1")
|
||||
except RegexException, e:
|
||||
logSys.error(e)
|
||||
raise e
|
||||
|
@ -188,6 +197,47 @@ class Filter(JailThread):
|
|||
def getFindTime(self):
|
||||
return self.__findTime
|
||||
|
||||
##
|
||||
# Set the date detector pattern, removing Defaults
|
||||
#
|
||||
# @param pattern the date template pattern
|
||||
|
||||
def setDatePattern(self, pattern):
|
||||
if pattern is None:
|
||||
self.dateDetector = None
|
||||
return
|
||||
elif pattern.upper() == "EPOCH":
|
||||
template = DateEpoch()
|
||||
template.name = "Epoch"
|
||||
elif pattern.upper() == "TAI64N":
|
||||
template = DateTai64n()
|
||||
template.name = "TAI64N"
|
||||
else:
|
||||
template = DatePatternRegex(pattern)
|
||||
self.dateDetector = DateDetector()
|
||||
self.dateDetector.appendTemplate(template)
|
||||
logSys.info("Date pattern set to `%r`: `%s`" %
|
||||
(pattern, template.name))
|
||||
logSys.debug("Date pattern regex for %r: %s" %
|
||||
(pattern, template.regex))
|
||||
|
||||
##
|
||||
# Get the date detector pattern, or Default Detectors if not changed
|
||||
#
|
||||
# @return pattern of the date template pattern
|
||||
|
||||
def getDatePattern(self):
|
||||
if self.dateDetector is not None:
|
||||
templates = self.dateDetector.templates
|
||||
if len(templates) > 1:
|
||||
return None, "Default Detectors"
|
||||
elif len(templates) == 1:
|
||||
if hasattr(templates[0], "pattern"):
|
||||
pattern = templates[0].pattern
|
||||
else:
|
||||
pattern = None
|
||||
return pattern, templates[0].name
|
||||
|
||||
##
|
||||
# Set the maximum retry value.
|
||||
#
|
||||
|
@ -205,6 +255,25 @@ class Filter(JailThread):
|
|||
def getMaxRetry(self):
|
||||
return self.failManager.getMaxRetry()
|
||||
|
||||
##
|
||||
# Set the maximum line buffer size.
|
||||
#
|
||||
# @param value the line buffer size
|
||||
|
||||
def setMaxLines(self, value):
|
||||
if int(value) <= 0:
|
||||
raise ValueError("maxlines must be integer greater than zero")
|
||||
self.__lineBufferSize = int(value)
|
||||
logSys.info("Set maxlines = %i" % self.__lineBufferSize)
|
||||
|
||||
##
|
||||
# Get the maximum line buffer size.
|
||||
#
|
||||
# @return the line buffer size
|
||||
|
||||
def getMaxLines(self):
|
||||
return self.__lineBufferSize
|
||||
|
||||
##
|
||||
# Main loop.
|
||||
#
|
||||
|
@ -306,44 +375,43 @@ class Filter(JailThread):
|
|||
return True
|
||||
|
||||
if self.__ignoreCommand:
|
||||
command = Action.replaceTag(self.__ignoreCommand, { 'ip': ip } )
|
||||
command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip } )
|
||||
logSys.debug('ignore command: ' + command)
|
||||
return Action.executeCmd(command)
|
||||
return CommandAction.executeCmd(command)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def processLine(self, line, returnRawHost=False, checkAllRegex=False):
|
||||
def processLine(self, line, date=None, returnRawHost=False,
|
||||
checkAllRegex=False):
|
||||
"""Split the time portion from log msg and return findFailures on them
|
||||
"""
|
||||
try:
|
||||
# Decode line to UTF-8
|
||||
l = line.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
l = line
|
||||
l = l.rstrip('\r\n')
|
||||
|
||||
logSys.log(7, "Working on line %r", l)
|
||||
timeMatch = self.dateDetector.matchTime(l)
|
||||
if timeMatch:
|
||||
# Lets split into time part and log part of the line
|
||||
timeLine = timeMatch.group()
|
||||
# Lets leave the beginning in as well, so if there is no
|
||||
# anchore at the beginning of the time regexp, we don't
|
||||
# at least allow injection. Should be harmless otherwise
|
||||
logLine = l[:timeMatch.start()] + l[timeMatch.end():]
|
||||
if date:
|
||||
tupleLine = line
|
||||
else:
|
||||
timeLine = l
|
||||
logLine = l
|
||||
return logLine, self.findFailure(timeLine, logLine, returnRawHost, checkAllRegex)
|
||||
l = line.rstrip('\r\n')
|
||||
logSys.log(7, "Working on line %r", line)
|
||||
|
||||
def processLineAndAdd(self, line):
|
||||
timeMatch = self.dateDetector.matchTime(l)
|
||||
if timeMatch:
|
||||
tupleLine = (
|
||||
l[:timeMatch.start()],
|
||||
l[timeMatch.start():timeMatch.end()],
|
||||
l[timeMatch.end():])
|
||||
else:
|
||||
tupleLine = (l, "", "")
|
||||
|
||||
return "".join(tupleLine[::2]), self.findFailure(
|
||||
tupleLine, date, returnRawHost, checkAllRegex)
|
||||
|
||||
def processLineAndAdd(self, line, date=None):
|
||||
"""Processes the line for failures and populates failManager
|
||||
"""
|
||||
for element in self.processLine(line)[1]:
|
||||
for element in self.processLine(line, date)[1]:
|
||||
failregex = element[0]
|
||||
ip = element[1]
|
||||
unixTime = element[2]
|
||||
lines = element[3]
|
||||
logSys.debug("Processing line with time:%s and ip:%s"
|
||||
% (unixTime, ip))
|
||||
if unixTime < MyTime.time() - self.getFindTime():
|
||||
|
@ -351,11 +419,11 @@ class Filter(JailThread):
|
|||
% (unixTime, MyTime.time(), self.getFindTime()))
|
||||
break
|
||||
if self.inIgnoreIPList(ip):
|
||||
logSys.debug("Ignore %s" % ip)
|
||||
logSys.info("[%s] Ignore %s" % (self.jail.name, ip))
|
||||
continue
|
||||
logSys.debug("Found %s" % ip)
|
||||
logSys.info("[%s] Found %s" % (self.jail.name, ip))
|
||||
## print "D: Adding a ticket for %s" % ((ip, unixTime, [line]),)
|
||||
self.failManager.addFailure(FailTicket(ip, unixTime, [line]))
|
||||
self.failManager.addFailure(FailTicket(ip, unixTime, lines))
|
||||
|
||||
##
|
||||
# Returns true if the line should be ignored.
|
||||
|
@ -364,9 +432,9 @@ class Filter(JailThread):
|
|||
# @param line: the line
|
||||
# @return: a boolean
|
||||
|
||||
def ignoreLine(self, line):
|
||||
def ignoreLine(self, tupleLines):
|
||||
for ignoreRegexIndex, ignoreRegex in enumerate(self.__ignoreRegex):
|
||||
ignoreRegex.search(line)
|
||||
ignoreRegex.search(tupleLines)
|
||||
if ignoreRegex.hasMatched():
|
||||
return ignoreRegexIndex
|
||||
return None
|
||||
|
@ -378,55 +446,94 @@ class Filter(JailThread):
|
|||
# to find the logging time.
|
||||
# @return a dict with IP and timestamp.
|
||||
|
||||
def findFailure(self, timeLine, logLine,
|
||||
returnRawHost=False, checkAllRegex=False):
|
||||
logSys.log(5, "Date: %r, message: %r", timeLine, logLine)
|
||||
def findFailure(self, tupleLine, date=None, returnRawHost=False,
|
||||
checkAllRegex=False):
|
||||
failList = list()
|
||||
|
||||
# Checks if we must ignore this line.
|
||||
if self.ignoreLine(logLine) is not None:
|
||||
if self.ignoreLine([tupleLine[::2]]) is not None:
|
||||
# The ignoreregex matched. Return.
|
||||
logSys.log(7, "Matched ignoreregex and was ignored")
|
||||
logSys.log(7, "Matched ignoreregex and was \"%s\" ignored",
|
||||
"".join(tupleLine[::2]))
|
||||
return failList
|
||||
date = self.dateDetector.getUnixTime(timeLine)
|
||||
|
||||
timeText = tupleLine[1]
|
||||
if date:
|
||||
self.__lastTimeText = timeText
|
||||
self.__lastDate = date
|
||||
elif timeText:
|
||||
|
||||
dateTimeMatch = self.dateDetector.getTime(timeText)
|
||||
|
||||
if dateTimeMatch is None:
|
||||
logSys.error("findFailure failed to parse timeText: " + timeText)
|
||||
date = self.__lastDate
|
||||
|
||||
else:
|
||||
# Lets split into time part and log part of the line
|
||||
date = dateTimeMatch[0]
|
||||
timeMatch = dateTimeMatch[1]
|
||||
|
||||
self.__lastTimeText = timeText
|
||||
self.__lastDate = date
|
||||
else:
|
||||
timeText = self.__lastTimeText or "".join(tupleLine[::2])
|
||||
date = self.__lastDate
|
||||
|
||||
self.__lineBuffer = (
|
||||
self.__lineBuffer + [tupleLine])[-self.__lineBufferSize:]
|
||||
|
||||
# Iterates over all the regular expressions.
|
||||
for failRegexIndex, failRegex in enumerate(self.__failRegex):
|
||||
failRegex.search(logLine)
|
||||
failRegex.search(self.__lineBuffer)
|
||||
if failRegex.hasMatched():
|
||||
# The failregex matched.
|
||||
logSys.log(7, "Matched %s", failRegex)
|
||||
# Checks if we must ignore this match.
|
||||
if self.ignoreLine(failRegex.getMatchedTupleLines()) \
|
||||
is not None:
|
||||
# The ignoreregex matched. Remove ignored match.
|
||||
self.__lineBuffer = failRegex.getUnmatchedTupleLines()
|
||||
logSys.log(7, "Matched ignoreregex and was ignored")
|
||||
if not checkAllRegex:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if date is None:
|
||||
logSys.debug("Found a match for %r but no valid date/time "
|
||||
"found for %r. Please file a detailed issue on"
|
||||
" https://github.com/fail2ban/fail2ban/issues "
|
||||
"in order to get support for this format."
|
||||
% (logLine, timeLine))
|
||||
logSys.warning(
|
||||
"Found a match for %r but no valid date/time "
|
||||
"found for %r. Please try setting a custom "
|
||||
"date pattern (see man page jail.conf(5)). "
|
||||
"If format is complex, please "
|
||||
"file a detailed issue on"
|
||||
" https://github.com/fail2ban/fail2ban/issues "
|
||||
"in order to get support for this format."
|
||||
% ("\n".join(failRegex.getMatchedLines()), timeText))
|
||||
else:
|
||||
self.__lineBuffer = failRegex.getUnmatchedTupleLines()
|
||||
try:
|
||||
host = failRegex.getHost()
|
||||
if returnRawHost:
|
||||
failList.append([failRegexIndex, host, date])
|
||||
failList.append([failRegexIndex, host, date,
|
||||
failRegex.getMatchedLines()])
|
||||
if not checkAllRegex:
|
||||
break
|
||||
else:
|
||||
ipMatch = DNSUtils.textToIp(host, self.__useDns)
|
||||
if ipMatch:
|
||||
for ip in ipMatch:
|
||||
failList.append([failRegexIndex, ip, date])
|
||||
failList.append([failRegexIndex, ip, date,
|
||||
failRegex.getMatchedLines()])
|
||||
if not checkAllRegex:
|
||||
break
|
||||
except RegexException, e: # pragma: no cover - unsure if reachable
|
||||
logSys.error(e)
|
||||
return failList
|
||||
|
||||
|
||||
##
|
||||
# Get the status of the filter.
|
||||
#
|
||||
# Get some informations about the filter state such as the total
|
||||
# number of failures.
|
||||
# @return a list with tuple
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Status of failures detected by filter.
|
||||
"""
|
||||
ret = [("Currently failed", self.failManager.size()),
|
||||
("Total failed", self.failManager.getFailTotal())]
|
||||
return ret
|
||||
|
@ -438,6 +545,7 @@ class FileFilter(Filter):
|
|||
Filter.__init__(self, jail, **kwargs)
|
||||
## The log file path.
|
||||
self.__logPath = []
|
||||
self.setLogEncoding("auto")
|
||||
|
||||
##
|
||||
# Add a log file path
|
||||
|
@ -448,7 +556,12 @@ class FileFilter(Filter):
|
|||
if self.containsLogPath(path):
|
||||
logSys.error(path + " already exists")
|
||||
else:
|
||||
container = FileContainer(path, tail)
|
||||
container = FileContainer(path, self.getLogEncoding(), tail)
|
||||
db = self.jail.database
|
||||
if db is not None:
|
||||
lastpos = db.addLog(self.jail, container)
|
||||
if lastpos and not tail:
|
||||
container.setPos(lastpos)
|
||||
self.__logPath.append(container)
|
||||
logSys.info("Added logfile = %s" % path)
|
||||
self._addLogPath(path) # backend specific
|
||||
|
@ -468,6 +581,9 @@ class FileFilter(Filter):
|
|||
for log in self.__logPath:
|
||||
if log.getFileName() == path:
|
||||
self.__logPath.remove(log)
|
||||
db = self.jail.database
|
||||
if db is not None:
|
||||
db.updateLog(self.jail, log)
|
||||
logSys.info("Removed logfile = %s" % path)
|
||||
self._delLogPath(path)
|
||||
return
|
||||
|
@ -497,6 +613,28 @@ class FileFilter(Filter):
|
|||
return True
|
||||
return False
|
||||
|
||||
##
|
||||
# Set the log file encoding
|
||||
#
|
||||
# @param encoding the encoding used with log files
|
||||
|
||||
def setLogEncoding(self, encoding):
|
||||
if encoding.lower() == "auto":
|
||||
encoding = locale.getpreferredencoding()
|
||||
codecs.lookup(encoding) # Raise LookupError if invalid codec
|
||||
for log in self.getLogPath():
|
||||
log.setEncoding(encoding)
|
||||
self.__encoding = encoding
|
||||
logSys.info("Set jail log file encoding to %s" % encoding)
|
||||
|
||||
##
|
||||
# Get the log file encoding
|
||||
#
|
||||
# @return log encoding value
|
||||
|
||||
def getLogEncoding(self):
|
||||
return self.__encoding
|
||||
|
||||
def getFileContainer(self, path):
|
||||
for log in self.__logPath:
|
||||
if log.getFileName() == path:
|
||||
|
@ -539,15 +677,21 @@ class FileFilter(Filter):
|
|||
# might occur leading at least to tests failures.
|
||||
while has_content:
|
||||
line = container.readline()
|
||||
if (line == "") or not self._isActive():
|
||||
if not line or not self.active:
|
||||
# The jail reached the bottom or has been stopped
|
||||
break
|
||||
self.processLineAndAdd(line)
|
||||
container.close()
|
||||
db = self.jail.database
|
||||
if db is not None:
|
||||
db.updateLog(self.jail, container)
|
||||
return True
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
ret = Filter.status(self)
|
||||
"""Status of Filter plus files being monitored.
|
||||
"""
|
||||
ret = super(FileFilter, self).status
|
||||
path = [m.getFileName() for m in self.getLogPath()]
|
||||
ret.append(("File list", path))
|
||||
return ret
|
||||
|
@ -570,18 +714,19 @@ except ImportError: # pragma: no cover
|
|||
|
||||
class FileContainer:
|
||||
|
||||
def __init__(self, filename, tail = False):
|
||||
def __init__(self, filename, encoding, tail = False):
|
||||
self.__filename = filename
|
||||
self.setEncoding(encoding)
|
||||
self.__tail = tail
|
||||
self.__handler = None
|
||||
# Try to open the file. Raises an exception if an error occured.
|
||||
handler = open(filename)
|
||||
handler = open(filename, 'rb')
|
||||
stats = os.fstat(handler.fileno())
|
||||
self.__ino = stats.st_ino
|
||||
try:
|
||||
firstLine = handler.readline()
|
||||
# Computes the MD5 of the first line.
|
||||
self.__hash = md5sum(firstLine).digest()
|
||||
self.__hash = md5sum(firstLine).hexdigest()
|
||||
# Start at the beginning of file if tail mode is off.
|
||||
if tail:
|
||||
handler.seek(0, 2)
|
||||
|
@ -594,11 +739,24 @@ class FileContainer:
|
|||
def getFileName(self):
|
||||
return self.__filename
|
||||
|
||||
def setEncoding(self, encoding):
|
||||
codecs.lookup(encoding) # Raises LookupError if invalid
|
||||
self.__encoding = encoding
|
||||
|
||||
def getEncoding(self):
|
||||
return self.__encoding
|
||||
|
||||
def getHash(self):
|
||||
return self.__hash
|
||||
|
||||
def getPos(self):
|
||||
return self.__pos
|
||||
|
||||
def setPos(self, value):
|
||||
self.__pos = value
|
||||
|
||||
def open(self):
|
||||
self.__handler = open(self.__filename)
|
||||
self.__handler = open(self.__filename, 'rb')
|
||||
# Set the file descriptor to be FD_CLOEXEC
|
||||
fd = self.__handler.fileno()
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
|
@ -612,14 +770,14 @@ class FileContainer:
|
|||
return False
|
||||
firstLine = self.__handler.readline()
|
||||
# Computes the MD5 of the first line.
|
||||
myHash = md5sum(firstLine).digest()
|
||||
myHash = md5sum(firstLine).hexdigest()
|
||||
## print "D: fn=%s hashes=%s/%s inos=%s/%s pos=%s rotate=%s" % (
|
||||
## self.__filename, self.__hash, myHash, stats.st_ino, self.__ino, self.__pos,
|
||||
## self.__hash != myHash or self.__ino != stats.st_ino)
|
||||
## sys.stdout.flush()
|
||||
# Compare hash and inode
|
||||
if self.__hash != myHash or self.__ino != stats.st_ino:
|
||||
logSys.debug("Log rotation detected for %s" % self.__filename)
|
||||
logSys.info("Log rotation detected for %s" % self.__filename)
|
||||
self.__hash = myHash
|
||||
self.__ino = stats.st_ino
|
||||
self.__pos = 0
|
||||
|
@ -630,7 +788,15 @@ class FileContainer:
|
|||
def readline(self):
|
||||
if self.__handler is None:
|
||||
return ""
|
||||
return self.__handler.readline()
|
||||
line = self.__handler.readline()
|
||||
try:
|
||||
line = line.decode(self.getEncoding(), 'strict')
|
||||
except UnicodeDecodeError:
|
||||
logSys.warning("Error decoding line from '%s' with '%s': %s" %
|
||||
(self.getFileName(), self.getEncoding(), `line`))
|
||||
if sys.version_info >= (3,): # In python3, must be decoded
|
||||
line = line.decode(self.getEncoding(), 'ignore')
|
||||
return line
|
||||
|
||||
def close(self):
|
||||
if not self.__handler is None:
|
||||
|
@ -643,6 +809,21 @@ class FileContainer:
|
|||
## sys.stdout.flush()
|
||||
|
||||
|
||||
##
|
||||
# JournalFilter class.
|
||||
#
|
||||
# Base interface class for systemd journal filters
|
||||
|
||||
class JournalFilter(Filter): # pragma: systemd no cover
|
||||
|
||||
def addJournalMatch(self, match): # pragma: no cover - Base class, not used
|
||||
pass
|
||||
|
||||
def delJournalMatch(self, match): # pragma: no cover - Base class, not used
|
||||
pass
|
||||
|
||||
def getJournalMatch(self, match): # pragma: no cover - Base class, not used
|
||||
return []
|
||||
|
||||
##
|
||||
# Utils class for DNS and IP handling.
|
||||
|
@ -662,9 +843,13 @@ class DNSUtils:
|
|||
Thanks to Kevin Drapel.
|
||||
"""
|
||||
try:
|
||||
return socket.gethostbyname_ex(dns)[2]
|
||||
return set(socket.gethostbyname_ex(dns)[2])
|
||||
except socket.error, e:
|
||||
logSys.warn("Unable to find a corresponding IP address for %s: %s"
|
||||
logSys.warning("Unable to find a corresponding IP address for %s: %s"
|
||||
% (dns, e))
|
||||
return list()
|
||||
except socket.error, e:
|
||||
logSys.warning("Socket error raised trying to resolve hostname %s: %s"
|
||||
% (dns, e))
|
||||
return list()
|
||||
dnsToIp = staticmethod(dnsToIp)
|
|
@ -23,14 +23,16 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
from failmanager import FailManagerEmpty
|
||||
from filter import FileFilter
|
||||
from mytime import MyTime
|
||||
import time, logging, fcntl
|
||||
|
||||
import time, logging, gamin, fcntl
|
||||
import gamin
|
||||
|
||||
from .failmanager import FailManagerEmpty
|
||||
from .filter import FileFilter
|
||||
from .mytime import MyTime
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.filter")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
##
|
||||
# Log reader class.
|
||||
|
@ -107,16 +109,15 @@ class FilterGamin(FileFilter):
|
|||
# @return True when the thread exits nicely
|
||||
|
||||
def run(self):
|
||||
self.setActive(True)
|
||||
# Gamin needs a loop to collect and dispatch events
|
||||
while self._isActive():
|
||||
if not self.getIdle():
|
||||
while self.active:
|
||||
if not self.idle:
|
||||
# We cannot block here because we want to be able to
|
||||
# exit.
|
||||
if self.monitor.event_pending():
|
||||
self.monitor.handle_events()
|
||||
time.sleep(self.getSleepTime())
|
||||
logSys.debug(self.jail.getName() + ": filter terminated")
|
||||
time.sleep(self.sleeptime)
|
||||
logSys.debug(self.jail.name + ": filter terminated")
|
||||
return True
|
||||
|
||||
|
|
@ -24,14 +24,14 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
from failmanager import FailManagerEmpty
|
||||
from filter import FileFilter
|
||||
from mytime import MyTime
|
||||
|
||||
import time, logging, os
|
||||
|
||||
from .failmanager import FailManagerEmpty
|
||||
from .filter import FileFilter
|
||||
from .mytime import MyTime
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.filter")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
##
|
||||
# Log reader class.
|
||||
|
@ -82,12 +82,11 @@ class FilterPoll(FileFilter):
|
|||
# @return True when the thread exits nicely
|
||||
|
||||
def run(self):
|
||||
self.setActive(True)
|
||||
while self._isActive():
|
||||
while self.active:
|
||||
if logSys.getEffectiveLevel() <= 6:
|
||||
logSys.log(6, "Woke up idle=%s with %d files monitored",
|
||||
self.getIdle(), len(self.getLogPath()))
|
||||
if not self.getIdle():
|
||||
self.idle, len(self.getLogPath()))
|
||||
if not self.idle:
|
||||
# Get file modification
|
||||
for container in self.getLogPath():
|
||||
filename = container.getFileName()
|
||||
|
@ -104,11 +103,11 @@ class FilterPoll(FileFilter):
|
|||
self.failManager.cleanup(MyTime.time())
|
||||
self.dateDetector.sortTemplate()
|
||||
self.__modified = False
|
||||
time.sleep(self.getSleepTime())
|
||||
time.sleep(self.sleeptime)
|
||||
else:
|
||||
time.sleep(self.getSleepTime())
|
||||
time.sleep(self.sleeptime)
|
||||
logSys.debug(
|
||||
(self.jail is not None and self.jail.getName() or "jailless") +
|
||||
(self.jail is not None and self.jail.name or "jailless") +
|
||||
" filter terminated")
|
||||
return True
|
||||
|
||||
|
@ -141,10 +140,10 @@ class FilterPoll(FileFilter):
|
|||
% (filename, e))
|
||||
self.__file404Cnt[filename] += 1
|
||||
if self.__file404Cnt[filename] > 2:
|
||||
logSys.warn("Too many errors. Setting the jail idle")
|
||||
logSys.warning("Too many errors. Setting the jail idle")
|
||||
if self.jail is not None:
|
||||
self.jail.setIdle(True)
|
||||
self.jail.idle = True
|
||||
else:
|
||||
logSys.warn("No jail is assigned to %s" % self)
|
||||
logSys.warning("No jail is assigned to %s" % self)
|
||||
self.__file404Cnt[filename] = 0
|
||||
return False
|
|
@ -24,13 +24,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y
|
|||
__license__ = "GPL"
|
||||
|
||||
import time, logging, pyinotify
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from os.path import dirname, sep as pathsep
|
||||
|
||||
from failmanager import FailManagerEmpty
|
||||
from filter import FileFilter
|
||||
from mytime import MyTime
|
||||
from .failmanager import FailManagerEmpty
|
||||
from .filter import FileFilter
|
||||
from .mytime import MyTime
|
||||
|
||||
|
||||
if not hasattr(pyinotify, '__version__') \
|
||||
|
@ -47,7 +46,7 @@ except Exception, e:
|
|||
% str(e))
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.filter")
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
##
|
||||
# Log reader class.
|
||||
|
@ -169,11 +168,10 @@ class FilterPyinotify(FileFilter):
|
|||
# loop is necessary
|
||||
|
||||
def run(self):
|
||||
self.setActive(True)
|
||||
self.__notifier = pyinotify.ThreadedNotifier(self.__monitor,
|
||||
ProcessPyinotify(self))
|
||||
self.__notifier.start()
|
||||
logSys.debug("pyinotifier started for %s.", self.jail.getName())
|
||||
logSys.debug("pyinotifier started for %s.", self.jail.name)
|
||||
# TODO: verify that there is nothing really to be done for
|
||||
# idle jails
|
||||
return True
|
|
@ -0,0 +1,259 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
|
||||
__author__ = "Steven Hiscocks"
|
||||
__copyright__ = "Copyright (c) 2013 Steven Hiscocks"
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging, datetime, time
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from systemd import journal
|
||||
if LooseVersion(getattr(journal, '__version__', "0")) < '204':
|
||||
raise ImportError("Fail2Ban requires systemd >= 204")
|
||||
|
||||
from .failmanager import FailManagerEmpty
|
||||
from .filter import JournalFilter
|
||||
from .mytime import MyTime
|
||||
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger("fail2ban.filter")
|
||||
|
||||
##
|
||||
# Journal reader class.
|
||||
#
|
||||
# This class reads from systemd journal and detects login failures or anything
|
||||
# else that matches a given regular expression. This class is instantiated by
|
||||
# a Jail object.
|
||||
|
||||
class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
||||
##
|
||||
# Constructor.
|
||||
#
|
||||
# Initialize the filter object with default values.
|
||||
# @param jail the jail object
|
||||
|
||||
def __init__(self, jail, **kwargs):
|
||||
JournalFilter.__init__(self, jail, **kwargs)
|
||||
self.__modified = False
|
||||
# Initialise systemd-journal connection
|
||||
self.__journal = journal.Reader(converters={'__CURSOR': lambda x: x})
|
||||
self.__matches = []
|
||||
self.setDatePattern(None)
|
||||
logSys.debug("Created FilterSystemd")
|
||||
|
||||
|
||||
##
|
||||
# Add a journal match filters from list structure
|
||||
#
|
||||
# @param matches list structure with journal matches
|
||||
|
||||
def _addJournalMatches(self, matches):
|
||||
if self.__matches:
|
||||
self.__journal.add_disjunction() # Add OR
|
||||
newMatches = []
|
||||
for match in matches:
|
||||
newMatches.append([])
|
||||
for match_element in match:
|
||||
self.__journal.add_match(match_element)
|
||||
newMatches[-1].append(match_element)
|
||||
self.__journal.add_disjunction()
|
||||
self.__matches.extend(newMatches)
|
||||
|
||||
##
|
||||
# Add a journal match filter
|
||||
#
|
||||
# @param match journalctl syntax matches in list structure
|
||||
|
||||
def addJournalMatch(self, match):
|
||||
newMatches = [[]]
|
||||
for match_element in match:
|
||||
if match_element == "+":
|
||||
newMatches.append([])
|
||||
else:
|
||||
newMatches[-1].append(match_element)
|
||||
try:
|
||||
self._addJournalMatches(newMatches)
|
||||
except ValueError:
|
||||
logSys.error(
|
||||
"Error adding journal match for: %r", " ".join(match))
|
||||
self.resetJournalMatches()
|
||||
raise
|
||||
else:
|
||||
logSys.info("Added journal match for: %r", " ".join(match))
|
||||
##
|
||||
# Reset a journal match filter called on removal or failure
|
||||
#
|
||||
# @return None
|
||||
|
||||
def resetJournalMatches(self):
|
||||
self.__journal.flush_matches()
|
||||
logSys.debug("Flushed all journal matches")
|
||||
match_copy = self.__matches[:]
|
||||
self.__matches = []
|
||||
try:
|
||||
self._addJournalMatches(match_copy)
|
||||
except ValueError:
|
||||
logSys.error("Error restoring journal matches")
|
||||
raise
|
||||
else:
|
||||
logSys.debug("Journal matches restored")
|
||||
|
||||
##
|
||||
# Delete a journal match filter
|
||||
#
|
||||
# @param match journalctl syntax matches
|
||||
|
||||
def delJournalMatch(self, match):
|
||||
if match in self.__matches:
|
||||
del self.__matches[self.__matches.index(match)]
|
||||
self.resetJournalMatches()
|
||||
else:
|
||||
raise ValueError("Match not found")
|
||||
logSys.info("Removed journal match for: %r" % " ".join(match))
|
||||
|
||||
##
|
||||
# Get current journal match filter
|
||||
#
|
||||
# @return journalctl syntax matches
|
||||
|
||||
def getJournalMatch(self):
|
||||
return self.__matches
|
||||
|
||||
##
|
||||
# Join group of log elements which may be a mix of bytes and strings
|
||||
#
|
||||
# @param elements list of strings and bytes
|
||||
# @return elements joined as string
|
||||
|
||||
@staticmethod
|
||||
def _joinStrAndBytes(elements):
|
||||
strElements = []
|
||||
for element in elements:
|
||||
if isinstance(element, str):
|
||||
strElements.append(element)
|
||||
else:
|
||||
strElements.append(str(element, errors='ignore'))
|
||||
return " ".join(strElements)
|
||||
|
||||
##
|
||||
# Format journal log entry into syslog style
|
||||
#
|
||||
# @param entry systemd journal entry dict
|
||||
# @return format log line
|
||||
|
||||
@staticmethod
|
||||
def formatJournalEntry(logentry):
|
||||
logelements = [""]
|
||||
if logentry.get('_HOSTNAME'):
|
||||
logelements.append(logentry['_HOSTNAME'])
|
||||
if logentry.get('SYSLOG_IDENTIFIER'):
|
||||
logelements.append(logentry['SYSLOG_IDENTIFIER'])
|
||||
if logentry.get('_PID'):
|
||||
logelements[-1] += ("[%i]" % logentry['_PID'])
|
||||
logelements[-1] += ":"
|
||||
elif logentry.get('_COMM'):
|
||||
logelements.append(logentry['_COMM'])
|
||||
if logentry.get('_PID'):
|
||||
logelements[-1] += ("[%i]" % logentry['_PID'])
|
||||
logelements[-1] += ":"
|
||||
if logelements[-1] == "kernel:":
|
||||
if '_SOURCE_MONOTONIC_TIMESTAMP' in logentry:
|
||||
monotonic = logentry.get('_SOURCE_MONOTONIC_TIMESTAMP')
|
||||
else:
|
||||
monotonic = logentry.get('__MONOTONIC_TIMESTAMP')[0]
|
||||
logelements.append("[%12.6f]" % monotonic.total_seconds())
|
||||
if isinstance(logentry.get('MESSAGE',''), list):
|
||||
logelements.append(" ".join(logentry['MESSAGE']))
|
||||
else:
|
||||
logelements.append(logentry.get('MESSAGE', ''))
|
||||
|
||||
try:
|
||||
logline = u" ".join(logelements)
|
||||
except UnicodeDecodeError:
|
||||
# Python 2, so treat as string
|
||||
logline = " ".join([str(logline) for logline in logelements])
|
||||
except TypeError:
|
||||
# Python 3, one or more elements bytes
|
||||
logSys.warning("Error decoding log elements from journal: %s" %
|
||||
repr(logelements))
|
||||
logline = self._joinStrAndBytes(logelements)
|
||||
|
||||
date = logentry.get('_SOURCE_REALTIME_TIMESTAMP',
|
||||
logentry.get('__REALTIME_TIMESTAMP'))
|
||||
logSys.debug("Read systemd journal entry: %r" %
|
||||
"".join([date.isoformat(), logline]))
|
||||
return (('', date.isoformat(), logline),
|
||||
time.mktime(date.timetuple()) + date.microsecond/1.0E6)
|
||||
|
||||
##
|
||||
# Main loop.
|
||||
#
|
||||
# Peridocily check for new journal entries matching the filter and
|
||||
# handover to FailManager
|
||||
|
||||
def run(self):
|
||||
|
||||
# Seek to now - findtime in journal
|
||||
start_time = datetime.datetime.now() - \
|
||||
datetime.timedelta(seconds=int(self.getFindTime()))
|
||||
self.__journal.seek_realtime(start_time)
|
||||
# Move back one entry to ensure do not end up in dead space
|
||||
# if start time beyond end of journal
|
||||
try:
|
||||
self.__journal.get_previous()
|
||||
except OSError:
|
||||
pass # Reading failure, so safe to ignore
|
||||
|
||||
while self.active:
|
||||
if not self.idle:
|
||||
while self.active:
|
||||
try:
|
||||
logentry = self.__journal.get_next()
|
||||
except OSError:
|
||||
logSys.warning(
|
||||
"Error reading line from systemd journal")
|
||||
continue
|
||||
if logentry:
|
||||
self.processLineAndAdd(
|
||||
*self.formatJournalEntry(logentry))
|
||||
self.__modified = True
|
||||
else:
|
||||
break
|
||||
if self.__modified:
|
||||
try:
|
||||
while True:
|
||||
ticket = self.failManager.toBan()
|
||||
self.jail.putFailTicket(ticket)
|
||||
except FailManagerEmpty:
|
||||
self.failManager.cleanup(MyTime.time())
|
||||
self.__modified = False
|
||||
self.__journal.wait(self.sleeptime)
|
||||
logSys.debug((self.jail is not None and self.jail.name
|
||||
or "jailless") +" filter terminated")
|
||||
return True
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
ret = super(FilterSystemd, self).status
|
||||
ret.append(("Journal matches",
|
||||
[" + ".join(" ".join(match) for match in self.__matches)]))
|
||||
return ret
|
|
@ -0,0 +1,232 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Cyril Jaquier
|
||||
|
||||
__author__ = "Cyril Jaquier, Lee Clemens, Yaroslav Halchenko"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
import Queue, logging
|
||||
|
||||
from .actions import Actions
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = logging.getLogger(__name__)
|
||||
|
||||
class Jail:
|
||||
"""Fail2Ban jail, which manages a filter and associated actions.
|
||||
|
||||
The class handles the initialisation of a filter, and actions. It's
|
||||
role is then to act as an interface between the filter and actions,
|
||||
passing bans detected by the filter, for the actions to then act upon.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name assigned to the jail.
|
||||
backend : str
|
||||
Backend to be used for filter. "auto" will attempt to pick
|
||||
the most preferred backend method. Default: "auto"
|
||||
db : Fail2BanDb
|
||||
Fail2Ban persistent database instance. Default: `None`
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name
|
||||
database
|
||||
filter
|
||||
actions
|
||||
idle
|
||||
status
|
||||
"""
|
||||
|
||||
#Known backends. Each backend should have corresponding __initBackend method
|
||||
# yoh: stored in a list instead of a tuple since only
|
||||
# list had .index until 2.6
|
||||
_BACKENDS = ['pyinotify', 'gamin', 'polling', 'systemd']
|
||||
|
||||
def __init__(self, name, backend = "auto", db=None):
|
||||
self.__db = db
|
||||
# 26 based on iptable chain name limit of 30 less len('f2b-')
|
||||
if len(name) >= 26:
|
||||
logSys.warning("Jail name %r might be too long and some commands "
|
||||
"might not function correctly. Please shorten"
|
||||
% name)
|
||||
self.__name = name
|
||||
self.__queue = Queue.Queue()
|
||||
self.__filter = None
|
||||
logSys.info("Creating new jail '%s'" % self.name)
|
||||
self._setBackend(backend)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%r)" % (self.__class__.__name__, self.name)
|
||||
|
||||
def _setBackend(self, backend):
|
||||
backend = backend.lower() # to assure consistent matching
|
||||
|
||||
backends = self._BACKENDS
|
||||
if backend != 'auto':
|
||||
# we have got strict specification of the backend to use
|
||||
if not (backend in self._BACKENDS):
|
||||
logSys.error("Unknown backend %s. Must be among %s or 'auto'"
|
||||
% (backend, backends))
|
||||
raise ValueError("Unknown backend %s. Must be among %s or 'auto'"
|
||||
% (backend, backends))
|
||||
# so explore starting from it till the 'end'
|
||||
backends = backends[backends.index(backend):]
|
||||
|
||||
for b in backends:
|
||||
initmethod = getattr(self, '_init%s' % b.capitalize())
|
||||
try:
|
||||
initmethod()
|
||||
if backend != 'auto' and b != backend:
|
||||
logSys.warning("Could only initiated %r backend whenever "
|
||||
"%r was requested" % (b, backend))
|
||||
else:
|
||||
logSys.info("Initiated %r backend" % b)
|
||||
self.__actions = Actions(self)
|
||||
return # we are done
|
||||
except ImportError, e:
|
||||
logSys.debug(
|
||||
"Backend %r failed to initialize due to %s" % (b, e))
|
||||
# log error since runtime error message isn't printed, INVALID COMMAND
|
||||
logSys.error(
|
||||
"Failed to initialize any backend for Jail %r" % self.name)
|
||||
raise RuntimeError(
|
||||
"Failed to initialize any backend for Jail %r" % self.name)
|
||||
|
||||
|
||||
def _initPolling(self):
|
||||
logSys.info("Jail '%s' uses poller" % self.name)
|
||||
from filterpoll import FilterPoll
|
||||
self.__filter = FilterPoll(self)
|
||||
|
||||
def _initGamin(self):
|
||||
# Try to import gamin
|
||||
import gamin
|
||||
logSys.info("Jail '%s' uses Gamin" % self.name)
|
||||
from filtergamin import FilterGamin
|
||||
self.__filter = FilterGamin(self)
|
||||
|
||||
def _initPyinotify(self):
|
||||
# Try to import pyinotify
|
||||
import pyinotify
|
||||
logSys.info("Jail '%s' uses pyinotify" % self.name)
|
||||
from filterpyinotify import FilterPyinotify
|
||||
self.__filter = FilterPyinotify(self)
|
||||
|
||||
def _initSystemd(self): # pragma: systemd no cover
|
||||
# Try to import systemd
|
||||
import systemd
|
||||
logSys.info("Jail '%s' uses systemd" % self.name)
|
||||
from filtersystemd import FilterSystemd
|
||||
self.__filter = FilterSystemd(self)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of jail.
|
||||
"""
|
||||
return self.__name
|
||||
|
||||
@property
|
||||
def database(self):
|
||||
"""The database used to store persistent data for the jail.
|
||||
"""
|
||||
return self.__db
|
||||
|
||||
@property
|
||||
def filter(self):
|
||||
"""The filter which the jail is using to monitor log files.
|
||||
"""
|
||||
return self.__filter
|
||||
|
||||
@property
|
||||
def actions(self):
|
||||
"""Actions object used to manage actions for jail.
|
||||
"""
|
||||
return self.__actions
|
||||
|
||||
@property
|
||||
def idle(self):
|
||||
"""A boolean indicating whether jail is idle.
|
||||
"""
|
||||
return self.filter.idle or self.actions.idle
|
||||
|
||||
@idle.setter
|
||||
def idle(self, value):
|
||||
self.filter.idle = value
|
||||
self.actions.idle = value
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""The status of the jail.
|
||||
"""
|
||||
return [
|
||||
("Filter", self.filter.status),
|
||||
("Actions", self.actions.status),
|
||||
]
|
||||
|
||||
def putFailTicket(self, ticket):
|
||||
"""Add a fail ticket to the jail.
|
||||
|
||||
Used by filter to add a failure for banning.
|
||||
"""
|
||||
self.__queue.put(ticket)
|
||||
if self.database is not None:
|
||||
self.database.addBan(self, ticket)
|
||||
|
||||
def getFailTicket(self):
|
||||
"""Get a fail ticket from the jail.
|
||||
|
||||
Used by actions to get a failure for banning.
|
||||
"""
|
||||
try:
|
||||
return self.__queue.get(False)
|
||||
except Queue.Empty:
|
||||
return False
|
||||
|
||||
def start(self):
|
||||
"""Start the jail, by starting filter and actions threads.
|
||||
|
||||
Once stated, also queries the persistent database to reinstate
|
||||
any valid bans.
|
||||
"""
|
||||
self.filter.start()
|
||||
self.actions.start()
|
||||
# Restore any previous valid bans from the database
|
||||
if self.database is not None:
|
||||
for ticket in self.database.getBans(
|
||||
jail=self, bantime=self.actions.getBanTime()):
|
||||
self.__queue.put(ticket)
|
||||
logSys.info("Jail '%s' started" % self.name)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the jail, by stopping filter and actions threads.
|
||||
"""
|
||||
self.filter.stop()
|
||||
self.actions.stop()
|
||||
self.filter.join()
|
||||
self.actions.join()
|
||||
logSys.info("Jail '%s' stopped" % self.name)
|
||||
|
||||
def is_alive(self):
|
||||
"""Check jail "is_alive" by checking filter and actions threads.
|
||||
"""
|
||||
return self.filter.is_alive() or self.actions.is_alive()
|
|
@ -0,0 +1,104 @@
|
|||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Cyril Jaquier, Yaroslav Halchenko"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
from threading import Lock
|
||||
from collections import Mapping
|
||||
|
||||
from ..exceptions import DuplicateJailException, UnknownJailException
|
||||
from .jail import Jail
|
||||
|
||||
|
||||
class Jails(Mapping):
|
||||
"""Handles the jails.
|
||||
|
||||
This class handles the jails. Creation, deletion or access to a jail
|
||||
must be done through this class. This class is thread-safe which is
|
||||
not the case of the jail itself, including filter and actions. This
|
||||
class is based on Mapping type, and the `add` method must be used to
|
||||
add additional jails.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.__lock = Lock()
|
||||
self._jails = dict()
|
||||
|
||||
def add(self, name, backend, db=None):
|
||||
"""Adds a jail.
|
||||
|
||||
Adds a new jail if not already present which should use the
|
||||
given backend.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of the jail.
|
||||
backend : str
|
||||
The backend to use.
|
||||
db : Fail2BanDb
|
||||
Fail2Ban's persistent database instance.
|
||||
|
||||
Raises
|
||||
------
|
||||
DuplicateJailException
|
||||
If jail name is already present.
|
||||
"""
|
||||
try:
|
||||
self.__lock.acquire()
|
||||
if name in self._jails:
|
||||
raise DuplicateJailException(name)
|
||||
else:
|
||||
self._jails[name] = Jail(name, backend, db)
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
def __getitem__(self, name):
|
||||
try:
|
||||
self.__lock.acquire()
|
||||
return self._jails[name]
|
||||
except KeyError:
|
||||
raise UnknownJailException(name)
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
def __delitem__(self, name):
|
||||
try:
|
||||
self.__lock.acquire()
|
||||
del self._jails[name]
|
||||
except KeyError:
|
||||
raise UnknownJailException(name)
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
def __len__(self):
|
||||
try:
|
||||
self.__lock.acquire()
|
||||
return len(self._jails)
|
||||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
def __iter__(self):
|
||||
try:
|
||||
self.__lock.acquire()
|
||||
return iter(self._jails)
|
||||
finally:
|
||||
self.__lock.release()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue