mirror of
https://github.com/fail2ban/fail2ban.git
synced 2026-03-11 08:55:31 +00:00
Merge branch 'master' into k0ka/firewalld
# Conflicts: # config/action.d/firewallcmd-ipset.conf # config/action.d/firewallcmd-multiport.conf # config/action.d/firewallcmd-new.conf
This commit is contained in:
commit
897d963ac8
106 changed files with 1759 additions and 1208 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
ChangeLog linguist-language=Markdown
|
||||
49
.github/ISSUE_TEMPLATE.md
vendored
49
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -1,49 +0,0 @@
|
|||
_We will be very grateful, if your problem was described as completely as possible,
|
||||
enclosing excerpts from logs (if possible within DEBUG mode, if no errors evident
|
||||
within INFO mode), and configuration in particular of effected relevant settings
|
||||
(e.g., with ` fail2ban-client -d | grep 'affected-jail-name' ` for a particular
|
||||
jail troubleshooting).
|
||||
Thank you in advance for the details, because such issues like "It does not work"
|
||||
alone could not help to resolve anything!
|
||||
Thanks! (remove this paragraph and other comments upon reading)_
|
||||
|
||||
### Environment:
|
||||
|
||||
_Fill out and check (`[x]`) the boxes which apply. If your Fail2Ban version is outdated,
|
||||
and you can't verify that the issue persists in the recent release, better seek support
|
||||
from the distribution you obtained Fail2Ban from_
|
||||
|
||||
- Fail2Ban version (including any possible distribution suffixes):
|
||||
- OS, including release name/version:
|
||||
- [ ] Fail2Ban installed via OS/distribution mechanisms
|
||||
- [ ] You have not applied any additional foreign patches to the codebase
|
||||
- [ ] Some customizations were done to the configuration (provide details below is so)
|
||||
|
||||
### The issue:
|
||||
|
||||
_Summary here_
|
||||
|
||||
#### Steps to reproduce
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
#### Observed behavior
|
||||
|
||||
#### Any additional information
|
||||
|
||||
### Configuration, dump and another helpful excerpts
|
||||
|
||||
#### Any customizations done to /etc/fail2ban/ configuration
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant parts of /var/log/fail2ban.log file:
|
||||
_preferably obtained while running fail2ban with `loglevel = 4`_
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant lines from monitored log files in question:
|
||||
|
||||
```
|
||||
```
|
||||
70
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
70
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report a bug within the fail2ban engines (not filters or jails)
|
||||
title: '[BR]: '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- Before reporting, please make sure to search the open and closed issues for any reports in the past.
|
||||
- Use this issue template to report a bug in the fail2ban engine (not in a filter or jail).
|
||||
- If you want to request a feature or a new filter, please use "Feature request" or "Filter request" instead.
|
||||
- If you have rather some question, please open or join to some discussion.
|
||||
|
||||
We will be very grateful, if your problem was described as completely as possible,
|
||||
enclosing excerpts from logs (if possible within DEBUG mode, if no errors evident
|
||||
within INFO mode), and configuration in particular of effected relevant settings
|
||||
(e.g., with ` fail2ban-client -d | grep 'affected-jail-name' ` for a particular
|
||||
jail troubleshooting).
|
||||
Thank you in advance for the details, because such issues like "It does not work"
|
||||
alone could not help to resolve anything!
|
||||
Thanks!
|
||||
(you can remove this paragraph and other comments upon reading)
|
||||
-->
|
||||
|
||||
### Environment:
|
||||
|
||||
<!--
|
||||
Fill out and check (`[x]`) the boxes which apply. If your Fail2Ban version is outdated,
|
||||
and you can't verify that the issue persists in the recent release, better seek support
|
||||
from the distribution you obtained Fail2Ban from
|
||||
-->
|
||||
|
||||
- Fail2Ban version <!-- including any possible distribution suffixes --> :
|
||||
- OS, including release name/version :
|
||||
- [ ] Fail2Ban installed via OS/distribution mechanisms
|
||||
- [ ] You have not applied any additional foreign patches to the codebase
|
||||
- [ ] Some customizations were done to the configuration (provide details below is so)
|
||||
|
||||
### The issue:
|
||||
|
||||
<!-- summary here -->
|
||||
|
||||
#### Steps to reproduce
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
#### Observed behavior
|
||||
|
||||
#### Any additional information
|
||||
|
||||
|
||||
### Configuration, dump and another helpful excerpts
|
||||
|
||||
#### Any customizations done to /etc/fail2ban/ configuration
|
||||
<!-- put your configuration excerpts between next 2 lines -->
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant parts of /var/log/fail2ban.log file:
|
||||
<!-- preferably obtained while running fail2ban with `loglevel = 4` -->
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant lines from monitored log files:
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
35
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea or an enhancement for this project
|
||||
title: '[RFE]: '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- Before requesting, please make sure to search the open and closed issues for any requests in the past.
|
||||
- Use this issue template to request a feature in the fail2ban engine (not a new filter or jail).
|
||||
- If you want to request a new filter or failregex, please use "Filter request" instead.
|
||||
- If you have rather some question, please open or join to some discussion.
|
||||
-->
|
||||
|
||||
#### Feature request type
|
||||
<!--
|
||||
Please provide a summary description of the feature request.
|
||||
-->
|
||||
|
||||
#### Description
|
||||
<!--
|
||||
Please describe the feature in more detail.
|
||||
-->
|
||||
|
||||
#### Considered alternatives
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
#### Any additional information
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
59
.github/ISSUE_TEMPLATE/filter_request.md
vendored
Normal file
59
.github/ISSUE_TEMPLATE/filter_request.md
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
name: Filter request
|
||||
about: Request a new jail or filter to be supported or existing filter extended with new failregex
|
||||
title: '[FR]: '
|
||||
labels: filter-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- Before requesting, please make sure to search the open and closed issues for any requests in the past.
|
||||
- Sometimes failregex have been already requested before but are not implemented yet due to various reasons.
|
||||
- If there are no hits for your concerns, please proceed otherwise add a comment to the related issue (also if it is closed).
|
||||
- If you want to request a new feature, please use "Feature request" instead.
|
||||
- If you have rather some question, please open or join to some discussion.
|
||||
-->
|
||||
|
||||
### Environment:
|
||||
|
||||
<!--
|
||||
Fill out and check (`[x]`) the boxes which apply.
|
||||
-->
|
||||
|
||||
- Fail2Ban version <!-- including any possible distribution suffixes --> :
|
||||
- OS, including release name/version :
|
||||
|
||||
#### Service, project or product which log or journal should be monitored
|
||||
|
||||
- Name of filter or jail in Fail2Ban (if already exists) :
|
||||
- Service, project or product name, including release name/version :
|
||||
- Repository or URL (if known) :
|
||||
- Service type :
|
||||
- Ports and protocols the service is listening :
|
||||
|
||||
#### Log or journal information
|
||||
<!-- Delete unrelated group -->
|
||||
|
||||
<!-- Log file -->
|
||||
|
||||
- Log file name(s) :
|
||||
|
||||
<!-- Systemd journal -->
|
||||
|
||||
- Journal identifier or unit name :
|
||||
|
||||
#### Any additional information
|
||||
|
||||
|
||||
### Relevant lines from monitored log files:
|
||||
|
||||
#### failures in sense of fail2ban filter (fail2ban must match):
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
|
||||
#### legitimate messages (fail2ban should not consider as failures):
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
56
.github/workflows/main.yml
vendored
56
.github/workflows/main.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11.0-alpha.1', pypy2, pypy3]
|
||||
fail-fast: false
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
|
|
@ -33,34 +33,66 @@ jobs:
|
|||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
- name: Grant systemd-journal access
|
||||
run: sudo usermod -a -G systemd-journal "$USER" || echo 'no systemd-journal access'
|
||||
|
||||
- name: Python version
|
||||
run: |
|
||||
F2B_PY=$(python -c "import sys; print(sys.version)")
|
||||
echo "Python: ${{ matrix.python-version }} -- $F2B_PY"
|
||||
echo "Python: ${{ matrix.python-version }} -- ${F2B_PY/$'\n'/ }"
|
||||
F2B_PYV=$(echo "${F2B_PY}" | grep -oP '^\d+(?:\.\d+)')
|
||||
F2B_PY=${F2B_PY:0:1}
|
||||
echo "Set F2B_PY=$F2B_PY"
|
||||
echo "Set F2B_PY=$F2B_PY, F2B_PYV=$F2B_PYV"
|
||||
echo "F2B_PY=$F2B_PY" >> $GITHUB_ENV
|
||||
echo "F2B_PYV=$F2B_PYV" >> $GITHUB_ENV
|
||||
# for GHA we need to monitor all journals, since it cannot be found using SYSTEM_ONLY(4):
|
||||
echo "F2B_SYSTEMD_DEFAULT_FLAGS=0" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [[ "$F2B_PY" = 3 ]]; then python -m pip install --upgrade pip || echo "can't upgrade pip"; fi
|
||||
if [[ "$F2B_PY" = 3 ]] && ! command -v 2to3x -v 2to3 > /dev/null; then
|
||||
pip install 2to3
|
||||
#pip install 2to3
|
||||
sudo apt-get -y install 2to3
|
||||
fi
|
||||
pip install systemd-python || echo 'systemd not available'
|
||||
pip install pyinotify || echo 'inotify not available'
|
||||
#sudo apt-get -y install python${F2B_PY/2/}-pyinotify || echo 'inotify not available'
|
||||
python -m pip install pyinotify || echo 'inotify not available'
|
||||
#sudo apt-get -y install python${F2B_PY/2/}-systemd || echo 'systemd not available'
|
||||
sudo apt-get -y install libsystemd-dev || echo 'systemd dependencies seems to be unavailable'
|
||||
python -m pip install systemd-python || echo 'systemd not available'
|
||||
|
||||
- name: Before scripts
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
# Manually execute 2to3 for now
|
||||
if [[ "$F2B_PY" = 3 ]]; then echo "2to3 ..." && ./fail2ban-2to3; fi
|
||||
_debug() { echo -n "$1 "; err=$("${@:2}" 2>&1) && echo 'OK' || echo -e "FAIL\n$err"; }
|
||||
# (debug) output current preferred encoding:
|
||||
python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))'
|
||||
|
||||
_debug 'Encodings:' python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))'
|
||||
# (debug) backend availabilities:
|
||||
echo 'Backends:'
|
||||
_debug '- systemd:' python -c 'from fail2ban.server.filtersystemd import FilterSystemd'
|
||||
#_debug '- systemd (root): ' sudo python -c 'from fail2ban.server.filtersystemd import FilterSystemd'
|
||||
_debug '- pyinotify:' python -c 'from fail2ban.server.filterpyinotify import FilterPyinotify'
|
||||
|
||||
- name: Test suite
|
||||
run: if [[ "$F2B_PY" = 2 ]]; then python setup.py test; else python bin/fail2ban-testcases --verbosity=2; fi
|
||||
|
||||
run: |
|
||||
if [[ "$F2B_PY" = 2 ]]; then
|
||||
python setup.py test
|
||||
elif dpkg --compare-versions "$F2B_PYV" lt 3.10; then
|
||||
python bin/fail2ban-testcases --verbosity=2
|
||||
else
|
||||
echo "Skip systemd backend since systemd-python module must be fixed for python >= v.3.10 in GHA ..."
|
||||
python bin/fail2ban-testcases --verbosity=2 -i "[sS]ystemd|[jJ]ournal"
|
||||
fi
|
||||
|
||||
#- name: Test suite (debug some systemd tests only)
|
||||
#run: python bin/fail2ban-testcases --verbosity=2 "[sS]ystemd|[jJ]ournal"
|
||||
#run: python bin/fail2ban-testcases --verbosity=2 -l 5 "test_WrongChar"
|
||||
|
||||
- name: Build
|
||||
run: python setup.py build
|
||||
|
||||
#- name: Test initd scripts
|
||||
# run: shellcheck -s bash -e SC1090,SC1091 files/debian-initd
|
||||
|
|
|
|||
10
.travis.yml
10
.travis.yml
|
|
@ -10,16 +10,8 @@ dist: xenial
|
|||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: 2.6
|
||||
dist: trusty # required for Python 2.6
|
||||
- python: 2.7
|
||||
dist: trusty # required for packages like gamin
|
||||
name: 2.7 (trusty)
|
||||
- python: 2.7
|
||||
name: 2.7 (xenial)
|
||||
- python: pypy
|
||||
- python: 3.3
|
||||
dist: trusty
|
||||
#- python: pypy
|
||||
- python: 3.4
|
||||
- python: 3.5
|
||||
- python: 3.6
|
||||
|
|
|
|||
18
ChangeLog
18
ChangeLog
|
|
@ -10,17 +10,33 @@ ver. 1.0.1-dev-1 (20??/??/??) - development nightly edition
|
|||
-----------
|
||||
|
||||
### Compatibility:
|
||||
* potential incompatibility by parsing of options of `backend`, `filter` and `action` parameters (if they
|
||||
are partially incorrect), because fail2ban could throw an error now (doesn't silently bypass it anymore).
|
||||
* to v.0.11:
|
||||
- due to change of `actioncheck` behavior (gh-488), some actions can be incompatible as regards
|
||||
the invariant check, if `actionban` or `actionunban` would not throw an error (exit code
|
||||
different from 0) in case of unsane environment.
|
||||
|
||||
### Fixes
|
||||
* readline fixed to consider interim new-line character as part of code point in multi-byte logs
|
||||
(e. g. unicode encoding like utf-16be, utf-16le);
|
||||
* `action.d/ufw.conf`:
|
||||
- fixed handling on IPv6 (using prepend, gh-2331, gh-3018)
|
||||
- application names containing spaces can be used now (gh-656, gh-1532, gh-3018)
|
||||
* `filter.d/drupal-auth.conf` more strict regex, extended to match "Login attempt failed from" (gh-2742)
|
||||
|
||||
### New Features and Enhancements
|
||||
* `actioncheck` behavior is changed now (gh-488), so invariant check as well as restore or repair
|
||||
of sane environment (in case of recognized unsane state) would only occur on action errors (e. g.
|
||||
if ban or unban operations are exiting with other code as 0)
|
||||
* better recognition of log rotation, better performance by reopen: avoid unnecessary seek to begin of file
|
||||
(and hash calculation)
|
||||
* file filter reads only complete lines (ended with new-line) now, so waits for end of line (for its completion)
|
||||
* `action.d/ufw.conf` (gh-3018):
|
||||
- new option `add` (default `prepend`), can be supplied as `insert 1` for ufw versions before v.0.36 (gh-2331, gh-3018)
|
||||
- new options `kill-mode` and `kill` to drop established connections of intruder (see action for details, gh-3018)
|
||||
* `filter.d/nginx-http-auth.conf` - extended with parameter mode, so additionally to `auth` (or `normal`)
|
||||
mode `fallback` (or combined as `aggressive`) can find SSL errors while SSL handshaking, gh-2881
|
||||
|
||||
|
||||
ver. 0.11.2 (2020/11/23) - heal-the-world-with-security-tools
|
||||
|
|
@ -206,7 +222,7 @@ Yes, Hrrrm...
|
|||
### New Features
|
||||
* new replacement tags for failregex to match subnets in form of IP-addresses with CIDR mask (gh-2559):
|
||||
- `<CIDR>` - helper regex to match CIDR (simple integer form of net-mask);
|
||||
- `<SUBNET>` - regex to match sub-net adresses (in form of IP/CIDR, also single IP is matched, so part /CIDR is optional);
|
||||
- `<SUBNET>` - regex to match sub-net addresses (in form of IP/CIDR, also single IP is matched, so part /CIDR is optional);
|
||||
* grouped tags (`<ADDR>`, `<HOST>`, `<SUBNET>`) recognize IP addresses enclosed in square brackets
|
||||
* new failregex-flag tag `<F-MLFGAINED>` for failregex, signaled that the access to service was gained
|
||||
(ATM used similar to tag `<F-NOFAIL>`, but it does not add the log-line to matches, gh-2279)
|
||||
|
|
|
|||
1
FILTERS
1
FILTERS
|
|
@ -278,6 +278,7 @@ to tune it. fail2ban-regex -D ... will present Debuggex URLs for the regexs
|
|||
and sample log files that you pass into it.
|
||||
|
||||
In general use when using regex debuggers for generating fail2ban filters:
|
||||
|
||||
* use regex from the ./fail2ban-regex output (to ensure all substitutions are
|
||||
done)
|
||||
* replace <HOST> with (?&.ipv4)
|
||||
|
|
|
|||
4
MANIFEST
4
MANIFEST
|
|
@ -3,10 +3,9 @@ bin/fail2ban-regex
|
|||
bin/fail2ban-server
|
||||
bin/fail2ban-testcases
|
||||
ChangeLog
|
||||
config/action.d/apprise.conf
|
||||
config/action.d/abuseipdb.conf
|
||||
config/action.d/apf.conf
|
||||
config/action.d/badips.conf
|
||||
config/action.d/badips.py
|
||||
config/action.d/blocklist_de.conf
|
||||
config/action.d/bsd-ipfw.conf
|
||||
config/action.d/cloudflare.conf
|
||||
|
|
@ -220,7 +219,6 @@ fail2ban/setup.py
|
|||
fail2ban-testcases-all
|
||||
fail2ban-testcases-all-python3
|
||||
fail2ban/tests/action_d/__init__.py
|
||||
fail2ban/tests/action_d/test_badips.py
|
||||
fail2ban/tests/action_d/test_smtp.py
|
||||
fail2ban/tests/actionstestcase.py
|
||||
fail2ban/tests/actiontestcase.py
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ this case, you should use that instead.**
|
|||
|
||||
Required:
|
||||
- [Python2 >= 2.6 or Python >= 3.2](https://www.python.org) or [PyPy](https://pypy.org)
|
||||
- python-setuptools, python-distutils or python3-setuptools for installation from source
|
||||
|
||||
Optional:
|
||||
- [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify), may require:
|
||||
|
|
|
|||
49
config/action.d/apprise.conf
Normal file
49
config/action.d/apprise.conf
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Fail2Ban configuration file
|
||||
#
|
||||
# Author: Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = printf %%b "The jail <name> as been started successfully." | <apprise> -t "[Fail2Ban] <name>: started on `uname -n`"
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = printf %%b "The jail <name> has been stopped." | <apprise> -t "[Fail2Ban] <name>: stopped on `uname -n`"
|
||||
|
||||
# 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 = printf %%b "The IP <ip> has just been banned by Fail2Ban after <failures> attempts against <name>" | <apprise> -n "warning" -t "[Fail2Ban] <name>: banned <ip> from `uname -n`"
|
||||
|
||||
# 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]
|
||||
|
||||
# Define location of the default apprise configuration file to use
|
||||
#
|
||||
config = /etc/fail2ban/apprise.conf
|
||||
#
|
||||
apprise = apprise -c "<config>"
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# Fail2ban reporting to badips.com
|
||||
#
|
||||
# Note: This reports an IP only and does not actually ban traffic. Use
|
||||
# another action in the same jail if you want bans to occur.
|
||||
#
|
||||
# Set the category to the appropriate value before use.
|
||||
#
|
||||
# To get see register and optional key to get personalised graphs see:
|
||||
# http://www.badips.com/blog/personalized-statistics-track-the-attackers-of-all-your-servers-with-one-key
|
||||
|
||||
[Definition]
|
||||
|
||||
actionban = curl --fail --user-agent "<agent>" http://www.badips.com/add/<category>/<ip>
|
||||
|
||||
[Init]
|
||||
|
||||
# Option: category
|
||||
# Notes.: Values are from the list here: http://www.badips.com/get/categories
|
||||
category =
|
||||
|
|
@ -1,391 +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.
|
||||
|
||||
import sys
|
||||
if sys.version_info < (2, 7): # pragma: no cover
|
||||
raise ImportError("badips.py action requires Python >= 2.7")
|
||||
import json
|
||||
import threading
|
||||
import logging
|
||||
if sys.version_info >= (3, ): # pragma: 2.x no cover
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.parse import urlencode
|
||||
from urllib.error import HTTPError
|
||||
else: # pragma: 3.x no cover
|
||||
from urllib2 import Request, urlopen, HTTPError
|
||||
from urllib import urlencode
|
||||
|
||||
from fail2ban.server.actions import Actions, ActionBase, BanTicket
|
||||
from fail2ban.helpers import splitwords, str2LogLevel
|
||||
|
||||
|
||||
|
||||
class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
|
||||
"""Fail2Ban action which reports 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)
|
||||
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 retrieve personal list
|
||||
of blacklist IPs.
|
||||
updateperiod : int, optional
|
||||
Time in seconds between updating bad IPs blacklist.
|
||||
Default 900 (15 minutes)
|
||||
loglevel : int/str, optional
|
||||
Log level of the message when an IP is (un)banned.
|
||||
Default `DEBUG`.
|
||||
Can be also supplied as two-value list (comma- or space separated) to
|
||||
provide level of the summary message when a group of IPs is (un)banned.
|
||||
Example `DEBUG,INFO`.
|
||||
agent : str, optional
|
||||
User agent transmitted to server.
|
||||
Default `Fail2Ban/ver.`
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If invalid `category`, `score`, `banaction` or `updateperiod`.
|
||||
"""
|
||||
|
||||
TIMEOUT = 10
|
||||
_badips = "https://www.badips.com"
|
||||
def _Request(self, url, **argv):
|
||||
return Request(url, headers={'User-Agent': self.agent}, **argv)
|
||||
|
||||
def __init__(self, jail, name, category, score=3, age="24h",
|
||||
banaction=None, bancategory=None, bankey=None, updateperiod=900,
|
||||
loglevel='DEBUG', agent="Fail2Ban", timeout=TIMEOUT):
|
||||
super(BadIPsAction, self).__init__(jail, name)
|
||||
|
||||
self.timeout = timeout
|
||||
self.agent = agent
|
||||
self.category = category
|
||||
self.score = score
|
||||
self.age = age
|
||||
self.banaction = banaction
|
||||
self.bancategory = bancategory or category
|
||||
self.bankey = bankey
|
||||
loglevel = splitwords(loglevel)
|
||||
self.sumloglevel = str2LogLevel(loglevel[-1])
|
||||
self.loglevel = str2LogLevel(loglevel[0])
|
||||
self.updateperiod = updateperiod
|
||||
|
||||
self._bannedips = set()
|
||||
# Used later for threading.Timer for updating badips
|
||||
self._timer = None
|
||||
|
||||
@staticmethod
|
||||
def isAvailable(timeout=1):
|
||||
try:
|
||||
response = urlopen(Request("/".join([BadIPsAction._badips]),
|
||||
headers={'User-Agent': "Fail2Ban"}), timeout=timeout)
|
||||
return True, ''
|
||||
except Exception as e: # pragma: no cover
|
||||
return False, e
|
||||
|
||||
def logError(self, response, what=''): # pragma: no cover - sporadical (502: Bad Gateway, etc)
|
||||
messages = {}
|
||||
try:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
except:
|
||||
pass
|
||||
self._logSys.error(
|
||||
"%s. badips.com response: '%s'", what,
|
||||
messages.get('err', 'Unknown'))
|
||||
|
||||
def getCategories(self, incParents=False):
|
||||
"""Get badips.com categories.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of categories.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
ValueError
|
||||
If badips.com response didn't contain necessary information
|
||||
"""
|
||||
try:
|
||||
response = urlopen(
|
||||
self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to fetch categories")
|
||||
raise
|
||||
else:
|
||||
response_json = json.loads(response.read().decode('utf-8'))
|
||||
if not 'categories' in response_json:
|
||||
err = "badips.com response lacked categories specification. Response was: %s" \
|
||||
% (response_json,)
|
||||
self._logSys.error(err)
|
||||
raise ValueError(err)
|
||||
categories = response_json['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
|
||||
|
||||
def getList(self, 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([self._badips, "get", "list", category, str(score)]),
|
||||
urlencode({'age': age})])
|
||||
if key:
|
||||
url = "&".join([url, urlencode({'key': key})])
|
||||
self._logSys.debug('badips.com: get list, url: %r', url)
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to fetch bad IP list")
|
||||
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 != "any" and 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:
|
||||
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
|
||||
self._jail.actions[self.banaction].ban(ai)
|
||||
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.log(self.loglevel,
|
||||
"Banned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
|
||||
def _unbanIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
|
||||
self._jail.actions[self.banaction].unban(ai)
|
||||
except Exception as e:
|
||||
self._logSys.error(
|
||||
"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.log(self.loglevel,
|
||||
"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
|
||||
s = self._bannedips - ips
|
||||
m = len(s)
|
||||
self._unbanIPs(s)
|
||||
# Add new IPs which are now listed
|
||||
s = ips - self._bannedips
|
||||
p = len(s)
|
||||
self._banIPs(s)
|
||||
if m != 0 or p != 0:
|
||||
self._logSys.log(self.sumloglevel,
|
||||
"Updated IPs for jail '%s' (-%d/+%d)",
|
||||
self._jail.name, m, p)
|
||||
self._logSys.debug(
|
||||
"Next update for jail '%' 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, str(aInfo['ip'])])
|
||||
self._logSys.debug('badips.com: ban, url: %r', url)
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to ban")
|
||||
raise
|
||||
else:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.debug(
|
||||
"Response from badips.com report: '%s'",
|
||||
messages['suc'])
|
||||
|
||||
Action = BadIPsAction
|
||||
|
|
@ -44,7 +44,7 @@ actioncheck =
|
|||
#actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
|
||||
# API v4
|
||||
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
|
||||
-d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
|
||||
-d '{"mode":"block","configuration":{"target":"<cftarget>","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
|
||||
<_cf_api_url>
|
||||
|
||||
# Option: actionunban
|
||||
|
|
@ -59,7 +59,7 @@ actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
|
|||
#actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
|
||||
# API v4
|
||||
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
|
||||
"<_cf_api_url>?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1¬es=Fail2Ban%%20<name>" \
|
||||
"<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1¬es=Fail2Ban%%20<name>" \
|
||||
| { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
|
||||
if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
|
||||
curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"
|
||||
|
|
@ -81,3 +81,8 @@ _cf_api_prms = -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' -H 'Conten
|
|||
cftoken =
|
||||
|
||||
cfuser =
|
||||
|
||||
cftarget = ip
|
||||
|
||||
[Init?family=inet6]
|
||||
cftarget = ip6
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ logpath = /dev/null
|
|||
# Notes.: Your system mail command. Is passed 2 args: subject and recipient
|
||||
# Values: CMD
|
||||
#
|
||||
mailcmd = mail -s
|
||||
mailcmd = mail -E 'set escape' -s
|
||||
|
||||
# Option: mailargs
|
||||
# Notes.: Additional arguments to mail command. e.g. for standard Unix mail:
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ tcpflags =
|
|||
# Notes.: Your system mail command. Is passed 2 args: subject and recipient
|
||||
# Values: CMD
|
||||
#
|
||||
mailcmd = mail -s
|
||||
mailcmd = mail -E 'set escape' -s
|
||||
|
||||
# Option: mailargs
|
||||
# Notes.: Additional arguments to mail command. e.g. for standard Unix mail:
|
||||
|
|
|
|||
|
|
@ -18,21 +18,46 @@ before = firewallcmd-common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
actionstart = ipset create <ipmset> hash:ip timeout <default-ipsettime> <familyopt>
|
||||
actionstart = <ipstype_<ipsettype>/actionstart>
|
||||
firewall-cmd --direct --add-rule <family> <table> <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype>
|
||||
|
||||
actionflush = ipset flush <ipmset>
|
||||
actionflush = <ipstype_<ipsettype>/actionflush>
|
||||
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> <table> <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype>
|
||||
<actionflush>
|
||||
ipset destroy <ipmset>
|
||||
<ipstype_<ipsettype>/actionstop>
|
||||
|
||||
actionban = ipset add <ipmset> <ip> timeout <ipsettime> -exist
|
||||
actionban = <ipstype_<ipsettype>/actionban>
|
||||
|
||||
# actionprolong = %(actionban)s
|
||||
|
||||
actionunban = <ipstype_<ipsettype>/actionunban>
|
||||
|
||||
[ipstype_ipset]
|
||||
|
||||
actionstart = ipset create <ipmset> hash:ip timeout <default-ipsettime> <familyopt>
|
||||
|
||||
actionflush = ipset flush <ipmset>
|
||||
|
||||
actionstop = ipset destroy <ipmset>
|
||||
|
||||
actionban = ipset add <ipmset> <ip> timeout <ipsettime> -exist
|
||||
|
||||
actionunban = ipset del <ipmset> <ip> -exist
|
||||
|
||||
[ipstype_firewalld]
|
||||
|
||||
actionstart = firewall-cmd --direct --new-ipset=<ipmset> --type=hash:ip --option=timeout=<default-ipsettime> <firewalld_familyopt>
|
||||
|
||||
# TODO: there doesn't seem to be an explicit way to invoke the ipset flush function using firewall-cmd
|
||||
actionflush =
|
||||
|
||||
actionstop = firewall-cmd --direct --delete-ipset=<ipmset>
|
||||
|
||||
actionban = firewall-cmd --ipset=<ipmset> --add-entry=<ip>
|
||||
|
||||
actionunban = firewall-cmd --ipset=<ipmset> --remove-entry=<ip>
|
||||
|
||||
[Init]
|
||||
|
||||
# Option: chain
|
||||
|
|
@ -56,6 +81,12 @@ ipsettime = 0
|
|||
# banaction = %(known/banaction)s[ipsettime='<timeout-bantime>']
|
||||
timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0)
|
||||
|
||||
# Option: ipsettype
|
||||
# Notes.: defines type of ipset used for match-set (firewalld or ipset)
|
||||
# Values: firewalld or ipset
|
||||
# Default: ipset
|
||||
ipsettype = ipset
|
||||
|
||||
# Option: actiontype
|
||||
# Notes.: defines additions to the blocking rule
|
||||
# Values: leave empty to block all attempts from the host
|
||||
|
|
@ -71,18 +102,20 @@ allports = -p <protocol>
|
|||
# Option: multiport
|
||||
# Notes.: addition to block access only to specific ports
|
||||
# Usage.: use in jail config: banaction = firewallcmd-ipset[actiontype=<multiport>]
|
||||
multiport = -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)"
|
||||
multiport = -p <protocol> -m multiport --dports <port>
|
||||
|
||||
ipmset = f2b-<name>
|
||||
familyopt =
|
||||
firewalld_familyopt =
|
||||
|
||||
[Init?family=inet6]
|
||||
|
||||
ipmset = f2b-<name>6
|
||||
familyopt = family inet6
|
||||
firewalld_familyopt = --option=family=inet6
|
||||
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Author: Edgar Hoch and Daniel Black
|
||||
# Author: Edgar Hoch, Daniel Black, Sergey Brester and Mihail Politaev
|
||||
# firewallcmd-new / iptables-ipset-proto6 combined for maximium goodness
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ before = firewallcmd-common.conf
|
|||
|
||||
actionstart = firewall-cmd --direct --add-chain <family> <table> f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> <table> f2b-<name> 1000 -j RETURN
|
||||
firewall-cmd --direct --add-rule <family> <table> <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> <table> <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> <table> <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> <table> <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
firewall-cmd --direct --remove-rules <family> <table> f2b-<name>
|
||||
firewall-cmd --direct --remove-chain <family> <table> f2b-<name>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ before = firewallcmd-common.conf
|
|||
|
||||
actionstart = firewall-cmd --direct --add-chain <family> <table> f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> <table> f2b-<name> 1000 -j RETURN
|
||||
firewall-cmd --direct --add-rule <family> <table> <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> <table> <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> <table> <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> <table> <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
firewall-cmd --direct --remove-rules <family> <table> f2b-<name>
|
||||
firewall-cmd --direct --remove-chain <family> <table> f2b-<name>
|
||||
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ actioncheck =
|
|||
|
||||
fwcmd_rich_rule = rule family='<family>' source address='<ip>' port port='$p' protocol='<protocol>' %(rich-suffix)s
|
||||
|
||||
actionban = ports="$(echo '<port>' | sed s/:/-/g)"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
actionban = ports="<port>"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
|
||||
actionunban = ports="$(echo '<port>' | sed s/:/-/g)"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
actionunban = ports="<port>"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
|
||||
rich-suffix = <rich-blocktype>
|
||||
|
|
@ -17,7 +17,7 @@ actionstart = printf %%b "Hi,\n
|
|||
The jail <name> has been started successfully.\n
|
||||
Output will be buffered until <lines> lines are available.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
|
||||
|
|
@ -28,13 +28,13 @@ actionstop = if [ -f <tmpfile> ]; then
|
|||
These hosts have been banned by Fail2Ban.\n
|
||||
`cat <tmpfile>`
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: Summary from <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: Summary from <fq-hostname>" <dest>
|
||||
rm <tmpfile>
|
||||
fi
|
||||
printf %%b "Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
|
|
@ -55,7 +55,7 @@ actionban = printf %%b "`date`: <ip> (<failures> failures)\n" >> <tmpfile>
|
|||
These hosts have been banned by Fail2Ban.\n
|
||||
`cat <tmpfile>`
|
||||
\nRegards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: Summary" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: Summary" <dest>
|
||||
rm <tmpfile>
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ actionunban =
|
|||
# Notes.: Your system mail command. Is passed 2 args: subject and recipient
|
||||
# Values: CMD
|
||||
#
|
||||
mailcmd = mail -s
|
||||
mailcmd = mail -E 'set escape' -s
|
||||
|
||||
# Default name of the chain
|
||||
#
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ norestored = 1
|
|||
actionstart = printf %%b "Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
|
||||
|
|
@ -29,7 +29,7 @@ actionstart = printf %%b "Hi,\n
|
|||
actionstop = printf %%b "Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
|
|
@ -49,7 +49,7 @@ actionban = printf %%b "Hi,\n
|
|||
Here is more information about <ip> :\n
|
||||
`%(_whois_command)s`\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ norestored = 1
|
|||
actionstart = printf %%b "Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
|
||||
|
|
@ -25,7 +25,7 @@ actionstart = printf %%b "Hi,\n
|
|||
actionstop = printf %%b "Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
|
|
@ -43,7 +43,7 @@ actionban = printf %%b "Hi,\n
|
|||
The IP <ip> has just been banned by Fail2Ban after
|
||||
<failures> attempts against <name>.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
|
|
|||
|
|
@ -84,8 +84,15 @@ srv_cfg_path = /etc/nginx/
|
|||
#srv_cmd = nginx -c %(srv_cfg_path)s/nginx.conf
|
||||
srv_cmd = nginx
|
||||
|
||||
# first test configuration is correct, hereafter send reload signal:
|
||||
blck_lst_reload = %(srv_cmd)s -qt; if [ $? -eq 0 ]; then
|
||||
# pid file (used to check nginx is running):
|
||||
srv_pid = /run/nginx.pid
|
||||
|
||||
# command used to check whether nginx is running and configuration is valid:
|
||||
srv_is_running = [ -f "%(srv_pid)s" ]
|
||||
srv_check_cmd = %(srv_is_running)s && %(srv_cmd)s -qt
|
||||
|
||||
# first test nginx is running and configuration is correct, hereafter send reload signal:
|
||||
blck_lst_reload = %(srv_check_cmd)s; if [ $? -eq 0 ]; then
|
||||
%(srv_cmd)s -s reload; if [ $? -ne 0 ]; then echo 'reload failed.'; fi;
|
||||
fi;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,16 +13,45 @@ actionstop =
|
|||
|
||||
actioncheck =
|
||||
|
||||
actionban = [ -n "<application>" ] && app="app <application>"
|
||||
ufw insert <insertpos> <blocktype> from <ip> to <destination> $app
|
||||
# ufw does "quickly process packets for which we already have a connection" in before.rules,
|
||||
# therefore all related sockets should be closed
|
||||
# actionban is using `ss` to do so, this only handles IPv4 and IPv6.
|
||||
|
||||
actionunban = [ -n "<application>" ] && app="app <application>"
|
||||
ufw delete <blocktype> from <ip> to <destination> $app
|
||||
actionban = if [ -n "<application>" ] && ufw app info "<application>"
|
||||
then
|
||||
ufw <add> <blocktype> from <ip> to <destination> app "<application>" comment "<comment>"
|
||||
else
|
||||
ufw <add> <blocktype> from <ip> to <destination> comment "<comment>"
|
||||
fi
|
||||
<kill>
|
||||
|
||||
actionunban = if [ -n "<application>" ] && ufw app info "<application>"
|
||||
then
|
||||
ufw delete <blocktype> from <ip> to <destination> app "<application>"
|
||||
else
|
||||
ufw delete <blocktype> from <ip> to <destination>
|
||||
fi
|
||||
|
||||
# Option: kill-mode
|
||||
# Notes.: can be set to ss or conntrack (may be extended later with other modes) to immediately drop all connections from banned IP, default empty (no kill)
|
||||
# Example: banaction = ufw[kill-mode=ss]
|
||||
kill-mode =
|
||||
|
||||
# intern conditional parameter used to provide killing mode after ban:
|
||||
_kill_ =
|
||||
_kill_ss = ss -K dst "[<ip>]"
|
||||
_kill_conntrack = conntrack -D -s "<ip>"
|
||||
|
||||
# Option: kill
|
||||
# Notes.: can be used to specify custom killing feature, by default depending on option kill-mode
|
||||
# Examples: banaction = ufw[kill='ss -K "( sport = :http || sport = :https )" dst "[<ip>]"']
|
||||
# banaction = ufw[kill='cutter "<ip>"']
|
||||
kill = <_kill_<kill-mode>>
|
||||
|
||||
[Init]
|
||||
# Option: insertpos
|
||||
# Notes.: The position number in the firewall list to insert the block rule
|
||||
insertpos = 1
|
||||
# Option: add
|
||||
# Notes.: can be set to "insert 1" to insert a rule at certain position (here 1):
|
||||
add = prepend
|
||||
|
||||
# Option: blocktype
|
||||
# Notes.: reject or deny
|
||||
|
|
@ -36,6 +65,10 @@ destination = any
|
|||
# Notes.: application from sudo ufw app list
|
||||
application =
|
||||
|
||||
# Option: comment
|
||||
# Notes.: comment for rule added by fail2ban
|
||||
comment = by Fail2Ban after <failures> attempts against <name>
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Author: Guilhem Lettron
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ socket = /var/run/fail2ban/fail2ban.sock
|
|||
#
|
||||
pidfile = /var/run/fail2ban/fail2ban.pid
|
||||
|
||||
# Option: allowipv6
|
||||
# Notes.: Allows IPv6 interface:
|
||||
# Default: auto
|
||||
# Values: [ auto yes (on, true, 1) no (off, false, 0) ] Default: auto
|
||||
#allowipv6 = auto
|
||||
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
[Definition]
|
||||
|
||||
failregex = ^<HOST> .*Googlebot.*$
|
||||
failregex = ^\s*<HOST> \S+ \S+(?: \S+)?\s+\S+ "[A-Z]+ /\S* [^"]*" \d+ \d+ \"[^"]*\" "[^"]*\bGooglebot/[^"]*"
|
||||
|
||||
ignoreregex =
|
||||
|
||||
datepattern = ^[^\[]*\[({DATE})
|
||||
datepattern = ^[^\[]*(\[{DATE}\s*\])
|
||||
{^LN-BEG}
|
||||
|
||||
# DEV Notes:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ before = apache-common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
failregex = ^%(_apache_error_client)s (?:(?:AH0013[456]: )?Invalid (method|URI) in request\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b)
|
||||
failregex = ^%(_apache_error_client)s (?:(?:AH001[23][456]: )?Invalid (method|URI) in request\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b)
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ log_prefix= (?:NOTICE|SECURITY|WARNING)%(__pid_re)s:?(?:\[C-[\da-f]*\])?:? [^:]+
|
|||
prefregex = ^%(__prefix_line)s%(log_prefix)s <F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
failregex = ^Registration from '[^']*' failed for '<HOST>(:\d+)?' - (?:Wrong password|Username/auth name mismatch|No matching peer found|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error \(permit/deny\)|Not a local domain)$
|
||||
^Call from '[^']*' \(<HOST>:\d+\) to extension '[^']*' rejected because extension not found in context
|
||||
^Call from '[^']*' \((?:(?:TCP|UDP):)?<HOST>:\d+\) to extension '[^']*' rejected because extension not found in context
|
||||
^(?:Host )?<HOST> (?:failed (?:to authenticate\b|MD5 authentication\b)|tried to authenticate with nonexistent user\b)
|
||||
^No registration for peer '[^']*' \(from <HOST>\)$
|
||||
^hacking attempt detected '<HOST>'$
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@ before = common.conf
|
|||
[Definition]
|
||||
|
||||
_auth_worker = (?:dovecot: )?auth(?:-worker)?
|
||||
_auth_worker_info = (?:conn \w+:auth(?:-worker)? \(uid=\w+\): auth(?:-worker)?<\d+>: )?
|
||||
_daemon = (?:dovecot(?:-auth)?|auth)
|
||||
|
||||
prefregex = ^%(__prefix_line)s(?:%(_auth_worker)s(?:\([^\)]+\))?: )?(?:%(__pam_auth)s(?:\(dovecot:auth\))?: |(?:pop3|imap|managesieve|submission)-login: )?(?:Info: )?<F-CONTENT>.+</F-CONTENT>$
|
||||
prefregex = ^%(__prefix_line)s(?:%(_auth_worker)s(?:\([^\)]+\))?: )?(?:%(__pam_auth)s(?:\(dovecot:auth\))?: |(?:pop3|imap|managesieve|submission)-login: )?(?:Info: )?%(_auth_worker_info)s<F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
failregex = ^authentication failure; logname=<F-ALT_USER1>\S*</F-ALT_USER1> uid=\S* euid=\S* tty=dovecot ruser=<F-USER>\S*</F-USER> rhost=<HOST>(?:\s+user=<F-ALT_USER>\S*</F-ALT_USER>)?\s*$
|
||||
^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?::(?: [^ \(]+)+)? \((?:auth failed, \d+ attempts(?: in \d+ secs)?|tried to use (?:disabled|disallowed) \S+ auth|proxy dest auth failed)\):(?: user=<<F-USER>[^>]*</F-USER>>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$
|
||||
^pam\(\S+,<HOST>(?:,\S*)?\): pam_authenticate\(\) failed: (?:User not known to the underlying authentication module: \d+ Time\(s\)|Authentication failure \(password mismatch\?\)|Permission denied)\s*$
|
||||
^[a-z\-]{3,15}\(\S*,<HOST>(?:,\S*)?\): (?:unknown user|invalid credentials|Password mismatch)
|
||||
^pam\(\S+,<HOST>(?:,\S*)?\): pam_authenticate\(\) failed: (?:User not known to the underlying authentication module: \d+ Time\(s\)|Authentication failure \([Pp]assword mismatch\?\)|Permission denied)\s*$
|
||||
^[a-z\-]{3,15}\(\S*,<HOST>(?:,\S*)?\): (?:[Uu]nknown user|[Ii]nvalid credentials|[Pp]assword mismatch)
|
||||
<mdre-<mode>>
|
||||
|
||||
mdre-aggressive = ^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?::(?: [^ \(]+)+)? \((?:no auth attempts|disconnected before auth was ready,|client didn't finish \S+ auth,)(?: (?:in|waited) \d+ secs)?\):(?: user=<[^>]*>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ before = common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
failregex = ^%(__prefix_line)s(https?:\/\/)([\da-z\.-]+)\.([a-z\.]{2,6})(\/[\w\.-]+)*\|\d{10}\|user\|<HOST>\|.+\|.+\|\d\|.*\|Login attempt failed for .+\.$
|
||||
failregex = ^%(__prefix_line)s(?:https?:\/\/)[^|]+\|[^|]+\|[^|]+\|<ADDR>\|(?:[^|]*\|)*Login attempt failed (?:for|from) <F-USER>[^|]+</F-USER>\.$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ after = exim-common.local
|
|||
host_info_pre = (?:H=([\w.-]+ )?(?:\(\S+\) )?)?
|
||||
host_info_suf = (?::\d+)?(?: I=\[\S+\](:\d+)?)?(?: U=\S+)?(?: P=e?smtp)?(?: F=(?:<>|[^@]+@\S+))?\s
|
||||
host_info = %(host_info_pre)s\[<HOST>\]%(host_info_suf)s
|
||||
pid = (?: \[\d+\])?
|
||||
pid = (?: \[\d+\]| \w+ exim\[\d+\]:)?
|
||||
|
||||
# DEV Notes:
|
||||
# From exim source code: ./src/receive.c:add_host_info_for_log
|
||||
|
|
|
|||
|
|
@ -6,24 +6,35 @@
|
|||
#
|
||||
import sys
|
||||
from fail2ban.server.ipdns import DNSUtils, IPAddr
|
||||
from threading import Thread
|
||||
|
||||
def process_args(argv):
|
||||
if len(argv) != 2:
|
||||
raise ValueError("Please provide a single IP as an argument. Got: %s\n"
|
||||
% (argv[1:]))
|
||||
if len(argv) - 1 not in (1, 2):
|
||||
raise ValueError("Usage %s ip ?timeout?. Got: %s\n"
|
||||
% (argv[0], argv[1:]))
|
||||
ip = argv[1]
|
||||
|
||||
if not IPAddr(ip).isValid:
|
||||
raise ValueError("Argument must be a single valid IP. Got: %s\n"
|
||||
% ip)
|
||||
return ip
|
||||
return argv[1:]
|
||||
|
||||
google_ips = None
|
||||
|
||||
def is_googlebot(ip):
|
||||
def is_googlebot(ip, timeout=55):
|
||||
import re
|
||||
|
||||
host = DNSUtils.ipToName(ip)
|
||||
timeout = float(timeout or 0)
|
||||
if timeout:
|
||||
def ipToNameTO(host, ip, timeout):
|
||||
host[0] = DNSUtils.ipToName(ip)
|
||||
host = [None]
|
||||
th = Thread(target=ipToNameTO, args=(host, ip, timeout)); th.daemon=True; th.start()
|
||||
th.join(timeout)
|
||||
host = host[0]
|
||||
else:
|
||||
host = DNSUtils.ipToName(ip)
|
||||
|
||||
if not host or not re.match(r'.*\.google(bot)?\.com$', host):
|
||||
return False
|
||||
host_ips = DNSUtils.dnsToIp(host)
|
||||
|
|
@ -31,7 +42,7 @@ def is_googlebot(ip):
|
|||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
try:
|
||||
ret = is_googlebot(process_args(sys.argv))
|
||||
ret = is_googlebot(*process_args(sys.argv))
|
||||
except ValueError as e:
|
||||
sys.stderr.write(str(e))
|
||||
sys.exit(2)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
[Definition]
|
||||
|
||||
failregex = ^: \((?:http|mod)_auth\.c\.\d+\) (?:password doesn\'t match .* username: .*|digest: auth failed for .*: wrong password|get_password failed), IP: <HOST>\s*$
|
||||
failregex = ^\s*(?:: )?\(?(?:http|mod)_auth\.c\.\d+\) (?:password doesn\'t match for (?:\S+|.*?) username:\s+<F-USER>(?:\S+|.*?)</F-USER>\s*|digest: auth failed(?: for\s+<F-ALT_USER>(?:\S+|.*?)</F-ALT_USER>\s*)?: (?:wrong password|uri mismatch \([^\)]*\))|get_password failed),? IP: <HOST>\s*$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
|
|
|||
25
config/filter.d/monitorix.conf
Normal file
25
config/filter.d/monitorix.conf
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Fail2Ban filter for Monitorix (HTTP built-in server)
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
_daemon = monitorix-httpd
|
||||
|
||||
# Option: failregex
|
||||
# Notes.: regex to match the password failures messages in the logfile. The
|
||||
# host must be matched by a group named "host". The tag "<HOST>" can
|
||||
# be used for standard IP/hostname matching and is only an alias for
|
||||
# (?:::f{4,6}:)?(?P<host>\S+)
|
||||
# Values: TEXT
|
||||
#
|
||||
failregex = ^(?:\s+-)?\s*(?:NOTEXIST|AUTHERR|NOTALLOWED) - <ADDR>\b
|
||||
|
||||
# Option: ignoreregex
|
||||
# Notes.: regex to ignore. If this regex matches, the line is ignored.
|
||||
# Values: TEXT
|
||||
#
|
||||
ignoreregex =
|
||||
15
config/filter.d/mssql-auth.conf
Normal file
15
config/filter.d/mssql-auth.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Fail2Ban filter for failed MSSQL Server authentication attempts
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^\s*Logon\s+Login failed for user '<F-USER>(?:[^']*|.*)</F-USER>'\. [^'\[]+\[CLIENT: <ADDR>\]$
|
||||
|
||||
|
||||
# DEV Notes:
|
||||
# Tested with SQL Server 2019 on Ubuntu 18.04
|
||||
#
|
||||
# Example:
|
||||
# 2020-02-24 14:48:55.12 Logon Login failed for user 'root'. Reason: Could not find a login matching the name provided. [CLIENT: 127.0.0.1]
|
||||
#
|
||||
# Author: Rüdiger Olschewsky
|
||||
#
|
||||
|
|
@ -11,4 +11,6 @@ datepattern = {^LN-BEG}%%ExY(?P<_sep>[-/.])%%m(?P=_sep)%%d[T ]%%H:%%M:%%S(?:[.,]
|
|||
^[^\[]*\[({DATE})
|
||||
{^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
# Author: Jan Przybylak
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ datepattern = {^LN-BEG}%%ExY(?P<_sep>[-/.])%%m(?P=_sep)%%d[T ]%%H:%%M:%%S(?:[.,]
|
|||
^[^\[]*\[({DATE})
|
||||
{^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
# DEV Notes:
|
||||
# Based on apache-botsearch filter
|
||||
#
|
||||
# Author: Frantisek Sumsal
|
||||
# Author: Frantisek Sumsal
|
||||
|
|
|
|||
|
|
@ -3,15 +3,32 @@
|
|||
|
||||
[Definition]
|
||||
|
||||
mode = normal
|
||||
|
||||
failregex = ^ \[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$
|
||||
mdre-auth = ^\s*\[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$
|
||||
mdre-fallback = ^\s*\[crit\] \d+#\d+: \*\d+ SSL_do_handshake\(\) failed \(SSL: error:\S+(?: \S+){1,3} too (?:long|short)\)[^,]*, client: <HOST>
|
||||
|
||||
mdre-normal = %(mdre-auth)s
|
||||
mdre-aggressive = %(mdre-auth)s
|
||||
%(mdre-fallback)s
|
||||
|
||||
failregex = <mdre-<mode>>
|
||||
|
||||
ignoreregex =
|
||||
|
||||
datepattern = {^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
# DEV NOTES:
|
||||
# mdre-auth:
|
||||
# Based on samples in https://github.com/fail2ban/fail2ban/pull/43/files
|
||||
# Extensive search of all nginx auth failures not done yet.
|
||||
#
|
||||
# Author: Daniel Black
|
||||
|
||||
# mdre-fallback:
|
||||
# Ban people checking for TLS_FALLBACK_SCSV repeatedly
|
||||
# https://stackoverflow.com/questions/28010492/nginx-critical-error-with-ssl-handshaking/28010608#28010608
|
||||
# Author: Stephan Orlowsky
|
||||
|
||||
|
|
|
|||
|
|
@ -44,3 +44,6 @@ failregex = ^\s*\[[a-z]+\] \d+#\d+: \*\d+ limiting requests, excess: [\d\.]+ by
|
|||
ignoreregex =
|
||||
|
||||
datepattern = {^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ _daemon = nsd
|
|||
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
|
||||
# Values: TEXT
|
||||
|
||||
failregex = ^%(__prefix_line)sinfo: ratelimit block .* query <HOST> TYPE255$
|
||||
^%(__prefix_line)sinfo: .* <HOST> refused, no acl matches\.$
|
||||
failregex = ^%(__prefix_line)sinfo: ratelimit block .* query <ADDR> TYPE255$
|
||||
^%(__prefix_line)sinfo: .* from(?: client)? <ADDR> refused, no acl matches\.?$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
datepattern = {^LN-BEG}Epoch
|
||||
{^LN-BEG}
|
||||
{^LN-BEG}
|
||||
|
|
|
|||
|
|
@ -12,16 +12,15 @@ before = common.conf
|
|||
|
||||
_daemon = postfix(-\w+)?/\w+(?:/smtp[ds])?
|
||||
_port = (?::\d+)?
|
||||
_pref = [A-Z]{4}
|
||||
|
||||
prefregex = ^%(__prefix_line)s<mdpr-<mode>> <F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
mdpr-normal = (?:\w+: reject:|(?:improper command pipelining|too many errors) after \S+)
|
||||
mdre-normal=^RCPT from [^[]*\[<HOST>\]%(_port)s: 55[04] 5\.7\.1\s
|
||||
^RCPT from [^[]*\[<HOST>\]%(_port)s: 45[04] 4\.7\.\d+ (?:Service unavailable\b|Client host rejected: cannot find your (reverse )?hostname\b)
|
||||
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.7\.\d+ (<[^>]*>)?: Helo command rejected: Host not found\b
|
||||
^EHLO from [^[]*\[<HOST>\]%(_port)s: 504 5\.5\.\d+ (<[^>]*>)?: Helo command rejected: need fully-qualified hostname\b
|
||||
^(RCPT|VRFY) from [^[]*\[<HOST>\]%(_port)s: 550 5\.1\.1\s
|
||||
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.1\.\d+ (<[^>]*>)?: Sender address rejected: Domain not found\b
|
||||
# Extended RE for normal mode to match reject by unknown users or undeliverable address, can be set to empty to avoid this:
|
||||
exre-user = |[Uu](?:ser unknown|ndeliverable address)
|
||||
|
||||
mdpr-normal = (?:\w+: (?:milter-)?reject:|(?:improper command pipelining|too many errors) after \S+)
|
||||
mdre-normal=^%(_pref)s from [^[]*\[<HOST>\]%(_port)s: [45][50][04] [45]\.\d\.\d+ (?:(?:<[^>]*>)?: )?(?:(?:Helo command|(?:Sender|Recipient) address) rejected: )?(?:Service unavailable|(?:Client host|Command|Data command) rejected|Relay access denied|(?:Host|Domain) not found|need fully-qualified hostname|match%(exre-user)s)\b
|
||||
^from [^[]*\[<HOST>\]%(_port)s:?
|
||||
|
||||
mdpr-auth = warning:
|
||||
|
|
@ -31,7 +30,7 @@ mdre-auth2= ^[^[]*\[<HOST>\]%(_port)s: SASL ((?i)LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5
|
|||
|
||||
# Mode "rbl" currently included in mode "normal", but if needed for jail "postfix-rbl" only:
|
||||
mdpr-rbl = %(mdpr-normal)s
|
||||
mdre-rbl = ^RCPT from [^[]*\[<HOST>\]%(_port)s: [45]54 [45]\.7\.1 Service unavailable; Client host \[\S+\] blocked\b
|
||||
mdre-rbl = ^%(_pref)s from [^[]*\[<HOST>\]%(_port)s: [45]54 [45]\.7\.1 Service unavailable; Client host \[\S+\] blocked\b
|
||||
|
||||
# Mode "rbl" currently included in mode "normal" (within 1st rule)
|
||||
mdpr-more = %(mdpr-normal)s
|
||||
|
|
@ -39,7 +38,7 @@ mdre-more = %(mdre-normal)s
|
|||
|
||||
# Includes some of the log messages described in
|
||||
# <http://www.postfix.org/POSTSCREEN_README.html>.
|
||||
mdpr-ddos = (?:lost connection after(?! DATA) [A-Z]+|disconnect(?= from \S+(?: \S+=\d+)* auth=0/(?:[1-9]|\d\d+))|(?:PREGREET \d+|HANGUP) after \S+)
|
||||
mdpr-ddos = (?:lost connection after(?! DATA) [A-Z]+|disconnect(?= from \S+(?: \S+=\d+)* auth=0/(?:[1-9]|\d\d+))|(?:PREGREET \d+|HANGUP) after \S+|COMMAND (?:TIME|COUNT|LENGTH) LIMIT)
|
||||
mdre-ddos = ^from [^[]*\[<HOST>\]%(_port)s:?
|
||||
|
||||
mdpr-extra = (?:%(mdpr-auth)s|%(mdpr-normal)s)
|
||||
|
|
|
|||
17
config/filter.d/scanlogd.conf
Normal file
17
config/filter.d/scanlogd.conf
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Fail2Ban filter for port scans detected by scanlogd
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
# Read common prefixes. If any customizations available -- read them from
|
||||
# common.local
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
_daemon = scanlogd
|
||||
|
||||
failregex = ^%(__prefix_line)s<ADDR>(?::<F-PORT/>)? to \S+ ports\b
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
|
||||
|
|
@ -15,7 +15,7 @@ addr = (?:IPv6:<IP6>|<IP4>)
|
|||
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID><F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
failregex = ^(\S+ )?\[%(addr)s\]( \(may be forged\))?: possible SMTP attack: command=AUTH, count=\d+$
|
||||
^AUTH failure \(LOGIN\):(?: [^:]+:)? authentication failure: checkpass failed, user=<F-USER>(?:\S+|.*?)</F-USER>, relay=(?:\S+ )?\[%(addr)s\](?: \(may be forged\))?$
|
||||
^AUTH failure \([^\)]+\):(?: [^:]+:)? (?:authentication failure|user not found): [^,]*, user=<F-USER>(?:\S+|.*?)</F-USER>, relay=(?:\S+ )?\[%(addr)s\](?: \(may be forged\))?$
|
||||
ignoreregex =
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=sendmail.service
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ before = common.conf
|
|||
|
||||
_daemon = (?:(sm-(mta|acceptingconnections)|sendmail))
|
||||
__prefix_line = %(known/__prefix_line)s(?:\w{14,20}: )?
|
||||
addr = (?:IPv6:<IP6>|<IP4>)
|
||||
addr = (?:(?:IPv6:)?<IP6>|<IP4>)
|
||||
|
||||
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID><F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
cmnfailre = ^ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[%(addr)s\](?: \(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))$
|
||||
^ruleset=check_relay, arg1=(?P<dom>\S+), arg2=%(addr)s, relay=((?P=dom) )?\[(\d+\.){3}\d+\](?: \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$
|
||||
cmnfailre = ^ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[%(addr)s\](?: \(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+\]|Fix reverse DNS for \S+)|553 5\.1\.8(?: (?P=email)\.\.\.)? Domain of sender address \S+ does not exist|550 5\.[71]\.1 (?P=email)\.\.\. (Rejected: .*|User unknown))$
|
||||
^ruleset=check_relay(?:, arg\d+=\S*)*, relay=(\S+ )?\[%(addr)s\](?: \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$
|
||||
^rejecting commands from (\S* )?\[%(addr)s\] due to pre-greeting traffic after \d+ seconds$
|
||||
^(?:\S+ )?\[%(addr)s\]: (?:(?i)expn|vrfy) \S+ \[rejected\]$
|
||||
^<[^@]+@[^>]+>\.\.\. No such user here$
|
||||
|
|
|
|||
|
|
@ -5,17 +5,23 @@ before = apache-common.conf
|
|||
|
||||
[Definition]
|
||||
|
||||
# pattern: [Wed Apr 27 23:12:07.736196 2016] [:error] [pid 2460] [client 10.1.1.1:47296] WAR [Login denied for user "test"], referer: https://zoneminderurl/index.php
|
||||
#
|
||||
# patterns: [Mon Mar 28 16:50:49.522240 2016] [:error] [pid 1795] [client 10.1.1.1:50700] WAR [Login denied for user "username1"], referer: https://zoneminder/
|
||||
# [Sun Mar 28 16:53:00.472693 2021] [php7:notice] [pid 11328] [client 10.1.1.1:39568] ERR [Could not retrieve user test details], referer: https://zm/
|
||||
# [Sun Mar 28 16:59:14.150625 2021] [php7:notice] [pid 11336] [client 10.1.1.1:39654] ERR [Login denied for user "john"], referer: https://zm/
|
||||
#
|
||||
# Option: failregex
|
||||
# Notes.: regex to match the password failure messages in the logfile.
|
||||
# Notes.: regex to match the login failure and non-existent user error messages in the logfile.
|
||||
|
||||
failregex = ^%(_apache_error_client)s WAR \[Login denied for user "[^"]*"\]
|
||||
prefregex = ^%(_apache_error_client)s (?:ERR|WAR) <F-CONTENT>\[(?:Login denied|Could not retrieve).*</F-CONTENT>$
|
||||
|
||||
failregex = ^\[Login denied for user "<F-USER>[^"]*</F-USER>"\]
|
||||
^\[Could not retrieve user <F-USER>\S*</F-USER>
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Notes:
|
||||
# Tested on Zoneminder 1.29.0
|
||||
# Tested on Zoneminder 1.29 and 1.35.21
|
||||
#
|
||||
# Zoneminder versions > 1.3x use "ERR" and < 1.3x use "WAR" level logs, so i've kept both for compatibility reasons
|
||||
#
|
||||
# Author: John Marzella
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ before = paths-debian.conf
|
|||
# more aggressive example of formula has the same values only for factor "2.0 / 2.885385" :
|
||||
#bantime.formula = ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)
|
||||
|
||||
# "bantime.multipliers" used to calculate next value of ban time instead of formula, coresponding
|
||||
# "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
|
||||
# previously ban count and given "bantime.factor" (for multipliers default is 1);
|
||||
# following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
|
||||
# always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
|
||||
|
|
@ -77,7 +77,7 @@ before = paths-debian.conf
|
|||
#bantime.multipliers = 1 5 30 60 300 720 1440 2880
|
||||
|
||||
# "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
|
||||
# cross over all jails, if false (dafault), only current jail of the ban IP will be searched
|
||||
# cross over all jails, if false (default), only current jail of the ban IP will be searched
|
||||
#bantime.overalljails = false
|
||||
|
||||
# --------------------
|
||||
|
|
@ -227,6 +227,15 @@ action_mwl = %(action_)s
|
|||
action_xarf = %(action_)s
|
||||
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"]
|
||||
|
||||
# ban & send a notification to one or more of the 50+ services supported by Apprise.
|
||||
# See https://github.com/caronc/apprise/wiki for details on what is supported.
|
||||
#
|
||||
# You may optionally over-ride the default configuration line (containing the Apprise URLs)
|
||||
# by using 'apprise[config="/alternate/path/to/apprise.cfg"]' otherwise
|
||||
# /etc/fail2ban/apprise.conf is sourced for your supported notification configuration.
|
||||
# action = %(action_)s
|
||||
# apprise
|
||||
|
||||
# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
|
||||
# to the destemail.
|
||||
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
|
||||
|
|
@ -242,20 +251,6 @@ action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
|
|||
#
|
||||
action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]
|
||||
|
||||
# Report ban via badips.com, and use as blacklist
|
||||
#
|
||||
# See BadIPsAction docstring in config/action.d/badips.py for
|
||||
# documentation for this action.
|
||||
#
|
||||
# NOTE: This action relies on banaction being present on start and therefore
|
||||
# should be last action defined for a jail.
|
||||
#
|
||||
action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"]
|
||||
#
|
||||
# Report ban via badips.com (uses action.d/badips.conf for reporting only)
|
||||
#
|
||||
action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"]
|
||||
|
||||
# Report ban via abuseipdb.com.
|
||||
#
|
||||
# See action.d/abuseipdb.conf for usage example and details.
|
||||
|
|
@ -375,8 +370,11 @@ banaction = %(banaction_allports)s
|
|||
logpath = /opt/openhab/logs/request.log
|
||||
|
||||
|
||||
# To use more aggressive http-auth modes set filter parameter "mode" in jail.local:
|
||||
# normal (default), aggressive (combines all), auth or fallback
|
||||
# See "tests/files/logs/nginx-http-auth" or "filter.d/nginx-http-auth.conf" for usage example and details.
|
||||
[nginx-http-auth]
|
||||
|
||||
# mode = normal
|
||||
port = http,https
|
||||
logpath = %(nginx_error_log)s
|
||||
|
||||
|
|
@ -397,7 +395,6 @@ logpath = %(nginx_error_log)s
|
|||
port = http,https
|
||||
logpath = %(nginx_access_log)s
|
||||
|
||||
|
||||
# Ban attackers that try to use PHP's URL-fopen() functionality
|
||||
# through GET/POST variables. - Experimental, with more than a year
|
||||
# of usage in production environments.
|
||||
|
|
@ -800,6 +797,14 @@ logpath = %(mysql_log)s
|
|||
backend = %(mysql_backend)s
|
||||
|
||||
|
||||
[mssql-auth]
|
||||
# Default configuration for Microsoft SQL Server for Linux
|
||||
# See the 'mssql-conf' manpage how to change logpath or port
|
||||
logpath = /var/opt/mssql/log/errorlog
|
||||
port = 1433
|
||||
filter = mssql-auth
|
||||
|
||||
|
||||
# Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf')
|
||||
[mongodb-auth]
|
||||
# change port when running with "--shardsvr" or "--configsvr" runtime operation
|
||||
|
|
@ -965,3 +970,11 @@ logpath = %(apache_error_log)s
|
|||
# see `filter.d/traefik-auth.conf` for details and service example.
|
||||
port = http,https
|
||||
logpath = /var/log/traefik/access.log
|
||||
|
||||
[scanlogd]
|
||||
logpath = %(syslog_local0)s
|
||||
banaction = %(banaction_allports)s
|
||||
|
||||
[monitorix]
|
||||
port = 8080
|
||||
logpath = /var/log/monitorix-httpd
|
||||
|
|
|
|||
|
|
@ -26,3 +26,5 @@ exim_main_log = /var/log/exim4/mainlog
|
|||
# was in debian squeezy but not in wheezy
|
||||
# /etc/proftpd/proftpd.conf (SystemLog)
|
||||
proftpd_log = /var/log/proftpd/proftpd.log
|
||||
|
||||
roundcube_errors_log = /var/log/roundcube/errors.log
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ class Fail2banCmdLine():
|
|||
cmdOpts = 'hc:s:p:xfbdtviqV'
|
||||
cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async',
|
||||
'conf=', 'pidfile=', 'pname=', 'socket=',
|
||||
'timeout=', 'str2sec=', 'help', 'version', 'dp', '--dump-pretty']
|
||||
'timeout=', 'str2sec=', 'help', 'version', 'dp', 'dump-pretty']
|
||||
optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts)
|
||||
except getopt.GetoptError:
|
||||
self.dispUsage()
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class Fail2banReader(ConfigReader):
|
|||
opts = [["string", "loglevel", "INFO" ],
|
||||
["string", "logtarget", "STDERR"],
|
||||
["string", "syslogsocket", "auto"],
|
||||
["string", "allowipv6", "auto"],
|
||||
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
|
||||
["int", "dbmaxmatches", None],
|
||||
["string", "dbpurgeage", "1d"]]
|
||||
|
|
@ -74,6 +75,7 @@ class Fail2banReader(ConfigReader):
|
|||
# Also dbfile should be set before all other database options.
|
||||
# So adding order indices into items, to be stripped after sorting, upon return
|
||||
order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13,
|
||||
"allowipv6": 14,
|
||||
"dbfile":50, "dbmaxmatches":51, "dbpurgeage":51}
|
||||
stream = list()
|
||||
for opt in self.__opts:
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ __license__ = "GPL"
|
|||
|
||||
import getopt
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
import time
|
||||
import urllib
|
||||
from optparse import OptionParser, Option
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ except ImportError:
|
|||
|
||||
from ..version import version, normVersion
|
||||
from .filterreader import FilterReader
|
||||
from ..server.filter import Filter, FileContainer
|
||||
from ..server.filter import Filter, FileContainer, MyTime
|
||||
from ..server.failregex import Regex, RegexException
|
||||
|
||||
from ..helpers import str2LogLevel, getVerbosityFormat, FormatterWithTraceBack, getLogger, \
|
||||
|
|
@ -269,15 +269,19 @@ class Fail2banRegex(object):
|
|||
self.setJournalMatch(shlex.split(opts.journalmatch))
|
||||
if opts.timezone:
|
||||
self._filter.setLogTimeZone(opts.timezone)
|
||||
self._filter.checkFindTime = False
|
||||
if True: # not opts.out:
|
||||
MyTime.setAlternateNow(0); # accept every date (years from 19xx up to end of current century, '%ExY' and 'Exy' patterns)
|
||||
from ..server.strptime import _updateTimeRE
|
||||
_updateTimeRE()
|
||||
if opts.datepattern:
|
||||
self.setDatePattern(opts.datepattern)
|
||||
if opts.usedns:
|
||||
self._filter.setUseDns(opts.usedns)
|
||||
self._filter.returnRawHost = opts.raw
|
||||
self._filter.checkFindTime = False
|
||||
self._filter.checkAllRegex = opts.checkAllRegex and not opts.out
|
||||
# ignore pending (without ID/IP), added to matches if it hits later (if ID/IP can be retreved)
|
||||
self._filter.ignorePending = opts.out
|
||||
self._filter.ignorePending = bool(opts.out)
|
||||
# callback to increment ignored RE's by index (during process):
|
||||
self._filter.onIgnoreRegex = self._onIgnoreRegex
|
||||
self._backend = 'auto'
|
||||
|
|
@ -285,9 +289,6 @@ class Fail2banRegex(object):
|
|||
def output(self, line):
|
||||
if not self._opts.out: output(line)
|
||||
|
||||
def decode_line(self, line):
|
||||
return FileContainer.decode_line('<LOG>', self._encoding, line)
|
||||
|
||||
def encode_line(self, line):
|
||||
return line.encode(self._encoding, 'ignore')
|
||||
|
||||
|
|
@ -326,26 +327,33 @@ class Fail2banRegex(object):
|
|||
regex = regextype + 'regex'
|
||||
# try to check - we've case filter?[options...]?:
|
||||
basedir = self._opts.config
|
||||
fltName = value
|
||||
fltFile = None
|
||||
fltOpt = {}
|
||||
if regextype == 'fail':
|
||||
fltName, fltOpt = extractOptions(value)
|
||||
if fltName is not None:
|
||||
if "." in fltName[~5:]:
|
||||
tryNames = (fltName,)
|
||||
else:
|
||||
tryNames = (fltName, fltName + '.conf', fltName + '.local')
|
||||
for fltFile in tryNames:
|
||||
if not "/" in fltFile:
|
||||
if os.path.basename(basedir) == 'filter.d':
|
||||
fltFile = os.path.join(basedir, fltFile)
|
||||
else:
|
||||
fltFile = os.path.join(basedir, 'filter.d', fltFile)
|
||||
if re.search(r'^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value):
|
||||
try:
|
||||
fltName, fltOpt = extractOptions(value)
|
||||
if "." in fltName[~5:]:
|
||||
tryNames = (fltName,)
|
||||
else:
|
||||
basedir = os.path.dirname(fltFile)
|
||||
if os.path.isfile(fltFile):
|
||||
break
|
||||
fltFile = None
|
||||
tryNames = (fltName, fltName + '.conf', fltName + '.local')
|
||||
for fltFile in tryNames:
|
||||
if not "/" in fltFile:
|
||||
if os.path.basename(basedir) == 'filter.d':
|
||||
fltFile = os.path.join(basedir, fltFile)
|
||||
else:
|
||||
fltFile = os.path.join(basedir, 'filter.d', fltFile)
|
||||
else:
|
||||
basedir = os.path.dirname(fltFile)
|
||||
if os.path.isfile(fltFile):
|
||||
break
|
||||
fltFile = None
|
||||
except Exception as e:
|
||||
output("ERROR: Wrong filter name or options: %s" % (str(e),))
|
||||
output(" while parsing: %s" % (value,))
|
||||
if self._verbose: raise(e)
|
||||
return False
|
||||
# if it is filter file:
|
||||
if fltFile is not None:
|
||||
if (basedir == self._opts.config
|
||||
|
|
@ -712,10 +720,6 @@ class Fail2banRegex(object):
|
|||
|
||||
return True
|
||||
|
||||
def file_lines_gen(self, hdlr):
|
||||
for line in hdlr:
|
||||
yield self.decode_line(line)
|
||||
|
||||
def start(self, args):
|
||||
|
||||
cmd_log, cmd_regex = args[:2]
|
||||
|
|
@ -734,10 +738,10 @@ class Fail2banRegex(object):
|
|||
|
||||
if os.path.isfile(cmd_log):
|
||||
try:
|
||||
hdlr = open(cmd_log, 'rb')
|
||||
test_lines = FileContainer(cmd_log, self._encoding, doOpen=True)
|
||||
|
||||
self.output( "Use log file : %s" % cmd_log )
|
||||
self.output( "Use encoding : %s" % self._encoding )
|
||||
test_lines = self.file_lines_gen(hdlr)
|
||||
except IOError as e: # pragma: no cover
|
||||
output( e )
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -140,9 +140,10 @@ class JailReader(ConfigReader):
|
|||
# Read filter
|
||||
flt = self.__opts["filter"]
|
||||
if flt:
|
||||
filterName, filterOpt = extractOptions(flt)
|
||||
if not filterName:
|
||||
raise JailDefError("Invalid filter definition %r" % flt)
|
||||
try:
|
||||
filterName, filterOpt = extractOptions(flt)
|
||||
except ValueError as e:
|
||||
raise JailDefError("Invalid filter definition %r: %s" % (flt, e))
|
||||
self.__filter = FilterReader(
|
||||
filterName, self.__name, filterOpt,
|
||||
share_config=self.share_config, basedir=self.getBaseDir())
|
||||
|
|
@ -174,10 +175,10 @@ class JailReader(ConfigReader):
|
|||
if not act: # skip empty actions
|
||||
continue
|
||||
# join with previous line if needed (consider possible new-line):
|
||||
actName, actOpt = extractOptions(act)
|
||||
prevln = ''
|
||||
if not actName:
|
||||
raise JailDefError("Invalid action definition %r" % act)
|
||||
try:
|
||||
actName, actOpt = extractOptions(act)
|
||||
except ValueError as e:
|
||||
raise JailDefError("Invalid action definition %r: %s" % (act, e))
|
||||
if actName.endswith(".py"):
|
||||
self.__actions.append([
|
||||
"set",
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ OPTION_CRE = re.compile(r"^([^\[]+)(?:\[(.*)\])?\s*$", re.DOTALL)
|
|||
# since v0.10 separator extended with `]\s*[` for support of multiple option groups, syntax
|
||||
# `action = act[p1=...][p2=...]`
|
||||
OPTION_EXTRACT_CRE = re.compile(
|
||||
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$)', re.DOTALL)
|
||||
r'\s*([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$|(?P<wrngA>.+))|,?\s*$|(?P<wrngB>.+)', re.DOTALL)
|
||||
# split by new-line considering possible new-lines within options [...]:
|
||||
OPTION_SPLIT_CRE = re.compile(
|
||||
r'(?:[^\[\s]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|\S+)(?=\n\s*|\s+|$)', re.DOTALL)
|
||||
|
|
@ -379,13 +379,19 @@ OPTION_SPLIT_CRE = re.compile(
|
|||
def extractOptions(option):
|
||||
match = OPTION_CRE.match(option)
|
||||
if not match:
|
||||
# TODO proper error handling
|
||||
return None, None
|
||||
raise ValueError("unexpected option syntax")
|
||||
option_name, optstr = match.groups()
|
||||
option_opts = dict()
|
||||
if optstr:
|
||||
for optmatch in OPTION_EXTRACT_CRE.finditer(optstr):
|
||||
if optmatch.group("wrngA"):
|
||||
raise ValueError("unexpected syntax at %d after option %r: %s" % (
|
||||
optmatch.start("wrngA"), optmatch.group(1), optmatch.group("wrngA")[0:25]))
|
||||
if optmatch.group("wrngB"):
|
||||
raise ValueError("expected option, wrong syntax at %d: %s" % (
|
||||
optmatch.start("wrngB"), optmatch.group("wrngB")[0:25]))
|
||||
opt = optmatch.group(1)
|
||||
if not opt: continue
|
||||
value = [
|
||||
val for val in optmatch.group(2,3,4) if val is not None][0]
|
||||
option_opts[opt.strip()] = value.strip()
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ protocol = [
|
|||
["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> datepattern", "gets the pattern used to match date/times for <JAIL>"],
|
||||
["get <JAIL> usedns", "gets the usedns setting for <JAIL>"],
|
||||
["get <JAIL> banip [<SEP>|--with-time]", "gets the list of of banned IP addresses for <JAIL>. Optionally the separator character ('<SEP>', default is space) or the option '--with-time' (printing the times of ban) may be specified. The IPs are ordered by end of ban."],
|
||||
["get <JAIL> maxretry", "gets the number of failures allowed for <JAIL>"],
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ import tempfile
|
|||
import threading
|
||||
import time
|
||||
from abc import ABCMeta
|
||||
from collections import MutableMapping
|
||||
try:
|
||||
from collections.abc import MutableMapping
|
||||
except ImportError:
|
||||
from collections import MutableMapping
|
||||
|
||||
from .failregex import mapTag2Opt
|
||||
from .ipdns import DNSUtils
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ import logging
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import Mapping
|
||||
try:
|
||||
from collections.abc import Mapping
|
||||
except ImportError:
|
||||
from collections import Mapping
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
|
|
@ -81,7 +84,7 @@ class Actions(JailThread, Mapping):
|
|||
self._jail = jail
|
||||
self._actions = OrderedDict()
|
||||
## The ban manager.
|
||||
self.__banManager = BanManager()
|
||||
self.banManager = BanManager()
|
||||
self.banEpoch = 0
|
||||
self.__lastConsistencyCheckTM = 0
|
||||
## Precedence of ban (over unban), so max number of tickets banned (to call an unban check):
|
||||
|
|
@ -200,7 +203,7 @@ class Actions(JailThread, Mapping):
|
|||
|
||||
def setBanTime(self, value):
|
||||
value = MyTime.str2seconds(value)
|
||||
self.__banManager.setBanTime(value)
|
||||
self.banManager.setBanTime(value)
|
||||
logSys.info(" banTime: %s" % value)
|
||||
|
||||
##
|
||||
|
|
@ -209,10 +212,10 @@ class Actions(JailThread, Mapping):
|
|||
# @return the time
|
||||
|
||||
def getBanTime(self):
|
||||
return self.__banManager.getBanTime()
|
||||
return self.banManager.getBanTime()
|
||||
|
||||
def getBanned(self, ids):
|
||||
lst = self.__banManager.getBanList()
|
||||
lst = self.banManager.getBanList()
|
||||
if not ids:
|
||||
return lst
|
||||
if len(ids) == 1:
|
||||
|
|
@ -227,7 +230,7 @@ class Actions(JailThread, Mapping):
|
|||
list
|
||||
The list of banned IP addresses.
|
||||
"""
|
||||
return self.__banManager.getBanList(ordered=True, withTime=withTime)
|
||||
return self.banManager.getBanList(ordered=True, withTime=withTime)
|
||||
|
||||
def addBannedIP(self, ip):
|
||||
"""Ban an IP or list of IPs."""
|
||||
|
|
@ -279,7 +282,7 @@ class Actions(JailThread, Mapping):
|
|||
if db and self._jail.database is not None:
|
||||
self._jail.database.delBan(self._jail, ip)
|
||||
# Find the ticket with the IP.
|
||||
ticket = self.__banManager.getTicketByID(ip)
|
||||
ticket = self.banManager.getTicketByID(ip)
|
||||
if ticket is not None:
|
||||
# Unban the IP.
|
||||
self.__unBan(ticket)
|
||||
|
|
@ -288,7 +291,7 @@ class Actions(JailThread, Mapping):
|
|||
if not isinstance(ip, IPAddr):
|
||||
ipa = IPAddr(ip)
|
||||
if not ipa.isSingle: # subnet (mask/cidr) or raw (may be dns/hostname):
|
||||
ips = filter(ipa.contains, self.__banManager.getBanList())
|
||||
ips = filter(ipa.contains, self.banManager.getBanList())
|
||||
if ips:
|
||||
return self.removeBannedIP(ips, db, ifexists)
|
||||
# not found:
|
||||
|
|
@ -347,7 +350,7 @@ class Actions(JailThread, Mapping):
|
|||
continue
|
||||
# wait for ban (stop if gets inactive, pending ban or unban):
|
||||
bancnt = 0
|
||||
wt = min(self.sleeptime, self.__banManager._nextUnbanTime - MyTime.time())
|
||||
wt = min(self.sleeptime, self.banManager._nextUnbanTime - MyTime.time())
|
||||
logSys.log(5, "Actions: wait for pending tickets %s (default %s)", wt, self.sleeptime)
|
||||
if Utils.wait_for(lambda: not self.active or self._jail.hasFailTickets, wt):
|
||||
bancnt = self.__checkBan()
|
||||
|
|
@ -394,7 +397,12 @@ class Actions(JailThread, Mapping):
|
|||
"ipfailures": lambda self: self._mi4ip(True).getAttempt(),
|
||||
"ipjailfailures": lambda self: self._mi4ip().getAttempt(),
|
||||
# raw ticket info:
|
||||
"raw-ticket": lambda self: repr(self.__ticket)
|
||||
"raw-ticket": lambda self: repr(self.__ticket),
|
||||
# jail info:
|
||||
"jail.banned": lambda self: self.__jail.actions.banManager.size(),
|
||||
"jail.banned_total": lambda self: self.__jail.actions.banManager.getBanTotal(),
|
||||
"jail.found": lambda self: self.__jail.filter.failManager.size(),
|
||||
"jail.found_total": lambda self: self.__jail.filter.failManager.getFailTotal()
|
||||
}
|
||||
|
||||
__slots__ = CallingMap.__slots__ + ('__ticket', '__jail', '__mi4ip')
|
||||
|
|
@ -491,11 +499,11 @@ class Actions(JailThread, Mapping):
|
|||
for ticket in tickets:
|
||||
|
||||
bTicket = BanTicket.wrap(ticket)
|
||||
btime = ticket.getBanTime(self.__banManager.getBanTime())
|
||||
btime = ticket.getBanTime(self.banManager.getBanTime())
|
||||
ip = bTicket.getIP()
|
||||
aInfo = self._getActionInfo(bTicket)
|
||||
reason = {}
|
||||
if self.__banManager.addBanTicket(bTicket, reason=reason):
|
||||
if self.banManager.addBanTicket(bTicket, reason=reason):
|
||||
cnt += 1
|
||||
# report ticket to observer, to check time should be increased and hereafter observer writes ban to database (asynchronous)
|
||||
if Observers.Main is not None and not bTicket.restored:
|
||||
|
|
@ -554,7 +562,7 @@ class Actions(JailThread, Mapping):
|
|||
# and increase ticket time if "bantime.increment" set)
|
||||
if cnt:
|
||||
logSys.debug("Banned %s / %s, %s ticket(s) in %r", cnt,
|
||||
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name)
|
||||
self.banManager.getBanTotal(), self.banManager.size(), self._jail.name)
|
||||
return cnt
|
||||
|
||||
def __reBan(self, ticket, actions=None, log=True):
|
||||
|
|
@ -594,7 +602,7 @@ class Actions(JailThread, Mapping):
|
|||
def _prolongBan(self, ticket):
|
||||
# prevent to prolong ticket that was removed in-between,
|
||||
# if it in ban list - ban time already prolonged (and it stays there):
|
||||
if not self.__banManager._inBanList(ticket): return
|
||||
if not self.banManager._inBanList(ticket): return
|
||||
# do actions :
|
||||
aInfo = None
|
||||
for name, action in self._actions.iteritems():
|
||||
|
|
@ -619,13 +627,13 @@ class Actions(JailThread, Mapping):
|
|||
|
||||
Unban IP addresses which are outdated.
|
||||
"""
|
||||
lst = self.__banManager.unBanList(MyTime.time(), maxCount)
|
||||
lst = self.banManager.unBanList(MyTime.time(), maxCount)
|
||||
for ticket in lst:
|
||||
self.__unBan(ticket)
|
||||
cnt = len(lst)
|
||||
if cnt:
|
||||
logSys.debug("Unbanned %s, %s ticket(s) in %r",
|
||||
cnt, self.__banManager.size(), self._jail.name)
|
||||
cnt, self.banManager.size(), self._jail.name)
|
||||
return cnt
|
||||
|
||||
def __flushBan(self, db=False, actions=None, stop=False):
|
||||
|
|
@ -639,10 +647,10 @@ class Actions(JailThread, Mapping):
|
|||
log = True
|
||||
if actions is None:
|
||||
logSys.debug(" Flush ban list")
|
||||
lst = self.__banManager.flushBanList()
|
||||
lst = self.banManager.flushBanList()
|
||||
else:
|
||||
log = False # don't log "[jail] Unban ..." if removing actions only.
|
||||
lst = iter(self.__banManager)
|
||||
lst = iter(self.banManager)
|
||||
cnt = 0
|
||||
# first we'll execute flush for actions supporting this operation:
|
||||
unbactions = {}
|
||||
|
|
@ -679,7 +687,7 @@ class Actions(JailThread, Mapping):
|
|||
self.__unBan(ticket, actions=actions, log=log)
|
||||
cnt += 1
|
||||
logSys.debug(" Unbanned %s, %s ticket(s) in %r",
|
||||
cnt, self.__banManager.size(), self._jail.name)
|
||||
cnt, self.banManager.size(), self._jail.name)
|
||||
return cnt
|
||||
|
||||
def __unBan(self, ticket, actions=None, log=True):
|
||||
|
|
@ -722,18 +730,18 @@ class Actions(JailThread, Mapping):
|
|||
logSys.warning("Unsupported extended jail status flavor %r. Supported: %s" % (flavor, supported_flavors))
|
||||
# Always print this information (basic)
|
||||
if flavor != "short":
|
||||
banned = self.__banManager.getBanList()
|
||||
banned = self.banManager.getBanList()
|
||||
cnt = len(banned)
|
||||
else:
|
||||
cnt = self.__banManager.size()
|
||||
cnt = self.banManager.size()
|
||||
ret = [("Currently banned", cnt),
|
||||
("Total banned", self.__banManager.getBanTotal())]
|
||||
("Total banned", self.banManager.getBanTotal())]
|
||||
if flavor != "short":
|
||||
ret += [("Banned IP list", banned)]
|
||||
if flavor == "cymru":
|
||||
cymru_info = self.__banManager.getBanListExtendedCymruInfo()
|
||||
cymru_info = self.banManager.getBanListExtendedCymruInfo()
|
||||
ret += \
|
||||
[("Banned ASN list", self.__banManager.geBanListExtendedASN(cymru_info)),
|
||||
("Banned Country list", self.__banManager.geBanListExtendedCountry(cymru_info)),
|
||||
("Banned RIR list", self.__banManager.geBanListExtendedRIR(cymru_info))]
|
||||
[("Banned ASN list", self.banManager.geBanListExtendedASN(cymru_info)),
|
||||
("Banned Country list", self.banManager.geBanListExtendedCountry(cymru_info)),
|
||||
("Banned RIR list", self.banManager.geBanListExtendedRIR(cymru_info))]
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -502,7 +502,7 @@ class Fail2BanDb(object):
|
|||
except TypeError:
|
||||
firstLineMD5 = None
|
||||
|
||||
if not firstLineMD5 and (pos or md5):
|
||||
if firstLineMD5 is None and (pos or md5 is not None):
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO logs(jail, path, firstlinemd5, lastfilepos) "
|
||||
"VALUES(?, ?, ?, ?)", (jail.name, name, md5, pos))
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from ..helpers import getLogger
|
|||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
logLevel = 6
|
||||
logLevel = 5
|
||||
|
||||
RE_DATE_PREMATCH = re.compile(r"(?<!\\)\{DATE\}", re.IGNORECASE)
|
||||
DD_patternCache = Utils.Cache(maxCount=1000, maxTime=60*60)
|
||||
|
|
@ -365,10 +365,10 @@ class DateDetector(object):
|
|||
# with space or some special char), otherwise possible collision/pattern switch:
|
||||
if ((
|
||||
line[distance-1:distance] == self.__lastPos[1] or
|
||||
(line[distance] == self.__lastPos[2] and not self.__lastPos[2].isalnum())
|
||||
(line[distance:distance+1] == self.__lastPos[2] and not self.__lastPos[2].isalnum())
|
||||
) and (
|
||||
line[endpos:endpos+1] == self.__lastEndPos[2] or
|
||||
(line[endpos-1] == self.__lastEndPos[1] and not self.__lastEndPos[1].isalnum())
|
||||
(line[endpos-1:endpos] == self.__lastEndPos[1] and not self.__lastEndPos[1].isalnum())
|
||||
)):
|
||||
# search in line part only:
|
||||
log(logLevel-1, " boundaries are correct, search in part %r", line[distance:endpos])
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class DateTemplate(object):
|
|||
# remove possible special pattern "**" in front and end of regex:
|
||||
regex = RE_DEL_WRD_BOUNDS[0].sub(RE_DEL_WRD_BOUNDS[1], regex)
|
||||
self._regex = regex
|
||||
logSys.log(7, ' constructed regex %s', regex)
|
||||
logSys.log(4, ' constructed regex %s', regex)
|
||||
self._cRegex = None
|
||||
|
||||
regex = property(getRegex, setRegex, doc=
|
||||
|
|
@ -159,6 +159,7 @@ class DateTemplate(object):
|
|||
"""
|
||||
if not self._cRegex:
|
||||
self._compileRegex()
|
||||
logSys.log(4, " search %s", self.regex)
|
||||
dateMatch = self._cRegex.search(line, *args); # pos, endpos
|
||||
if dateMatch:
|
||||
self.hits += 1
|
||||
|
|
|
|||
|
|
@ -127,9 +127,10 @@ class FailManager:
|
|||
return len(self.__failList)
|
||||
|
||||
def cleanup(self, time):
|
||||
time -= self.__maxTime
|
||||
with self.__lock:
|
||||
todelete = [fid for fid,item in self.__failList.iteritems() \
|
||||
if item.getTime() + self.__maxTime <= time]
|
||||
if item.getTime() <= time]
|
||||
if len(todelete) == len(self.__failList):
|
||||
# remove all:
|
||||
self.__failList = dict()
|
||||
|
|
@ -143,7 +144,7 @@ class FailManager:
|
|||
else:
|
||||
# create new dictionary without items to be deleted:
|
||||
self.__failList = dict((fid,item) for fid,item in self.__failList.iteritems() \
|
||||
if item.getTime() + self.__maxTime > time)
|
||||
if item.getTime() > time)
|
||||
self.__bgSvc.service()
|
||||
|
||||
def delFailure(self, fid):
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ class Filter(JailThread):
|
|||
## Store last time stamp, applicable for multi-line
|
||||
self.__lastTimeText = ""
|
||||
self.__lastDate = None
|
||||
## Next service (cleanup) time
|
||||
self.__nextSvcTime = -(1<<63)
|
||||
## if set, treat log lines without explicit time zone to be in this time zone
|
||||
self.__logtimezone = None
|
||||
## Default or preferred encoding (to decode bytes from file or journal):
|
||||
|
|
@ -103,6 +105,10 @@ class Filter(JailThread):
|
|||
## Error counter (protected, so can be used in filter implementations)
|
||||
## if it reached 100 (at once), run-cycle will go idle
|
||||
self._errors = 0
|
||||
## Next time to update log or journal position in database:
|
||||
self._nextUpdateTM = 0
|
||||
## Pending updates (must be executed at next update time or during stop):
|
||||
self._pendDBUpdates = {}
|
||||
## return raw host (host is not dns):
|
||||
self.returnRawHost = False
|
||||
## check each regex (used for test purposes):
|
||||
|
|
@ -115,10 +121,10 @@ class Filter(JailThread):
|
|||
self.checkFindTime = True
|
||||
## shows that filter is in operation mode (processing new messages):
|
||||
self.inOperation = True
|
||||
## if true prevents against retarded banning in case of RC by too many failures (disabled only for test purposes):
|
||||
self.banASAP = True
|
||||
## Ticks counter
|
||||
self.ticks = 0
|
||||
## Processed lines counter
|
||||
self.procLines = 0
|
||||
## Thread name:
|
||||
self.name="f2b/f."+self.jailName
|
||||
|
||||
|
|
@ -442,12 +448,23 @@ class Filter(JailThread):
|
|||
|
||||
def performBan(self, ip=None):
|
||||
"""Performs a ban for IPs (or given ip) that are reached maxretry of the jail."""
|
||||
try: # pragma: no branch - exception is the only way out
|
||||
while True:
|
||||
while True:
|
||||
try:
|
||||
ticket = self.failManager.toBan(ip)
|
||||
self.jail.putFailTicket(ticket)
|
||||
except FailManagerEmpty:
|
||||
self.failManager.cleanup(MyTime.time())
|
||||
except FailManagerEmpty:
|
||||
break
|
||||
self.jail.putFailTicket(ticket)
|
||||
if ip: break
|
||||
self.performSvc()
|
||||
|
||||
def performSvc(self, force=False):
|
||||
"""Performs a service tasks (clean failure list)."""
|
||||
tm = MyTime.time()
|
||||
# avoid too early clean up:
|
||||
if force or tm >= self.__nextSvcTime:
|
||||
self.__nextSvcTime = tm + 5
|
||||
# clean up failure list:
|
||||
self.failManager.cleanup(tm)
|
||||
|
||||
def addAttempt(self, ip, *matches):
|
||||
"""Generate a failed attempt for ip"""
|
||||
|
|
@ -606,6 +623,7 @@ class Filter(JailThread):
|
|||
noDate = False
|
||||
if date:
|
||||
tupleLine = line
|
||||
line = "".join(line)
|
||||
self.__lastTimeText = tupleLine[1]
|
||||
self.__lastDate = date
|
||||
else:
|
||||
|
|
@ -642,30 +660,37 @@ class Filter(JailThread):
|
|||
if self.__lastDate and self.__lastDate > MyTime.time() - 60:
|
||||
tupleLine = ("", self.__lastTimeText, line)
|
||||
date = self.__lastDate
|
||||
elif self.checkFindTime and self.inOperation:
|
||||
date = MyTime.time()
|
||||
|
||||
if self.checkFindTime:
|
||||
if self.checkFindTime and date is not None:
|
||||
# if in operation (modifications have been really found):
|
||||
if self.inOperation:
|
||||
# if weird date - we'd simulate now for timeing issue (too large deviation from now):
|
||||
if (date is None or date < MyTime.time() - 60 or date > MyTime.time() + 60):
|
||||
# log time zone issue as warning once per day:
|
||||
delta = int(date - MyTime.time())
|
||||
if abs(delta) > 60:
|
||||
# log timing issue as warning once per day:
|
||||
self._logWarnOnce("_next_simByTimeWarn",
|
||||
("Simulate NOW in operation since found time has too large deviation %s ~ %s +/- %s",
|
||||
date, MyTime.time(), 60),
|
||||
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
("Detected a log entry %s %s the current time in operation mode. "
|
||||
"This looks like a %s problem. Treating such entries as if they just happened.",
|
||||
MyTime.seconds2str(abs(delta)), "before" if delta < 0 else "after",
|
||||
"latency" if -3300 <= delta < 0 else "timezone"
|
||||
),
|
||||
("Please check a jail for a timing issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
# simulate now as date:
|
||||
date = MyTime.time()
|
||||
self.__lastDate = date
|
||||
else:
|
||||
# in initialization (restore) phase, if too old - ignore:
|
||||
if date is not None and date < MyTime.time() - self.getFindTime():
|
||||
if date < MyTime.time() - self.getFindTime():
|
||||
# log time zone issue as warning once per day:
|
||||
self._logWarnOnce("_next_ignByTimeWarn",
|
||||
("Ignore line since time %s < %s - %s",
|
||||
date, MyTime.time(), self.getFindTime()),
|
||||
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
("Ignoring all log entries older than %ss; these are probably" +
|
||||
" messages generated while fail2ban was not running.",
|
||||
self.getFindTime()),
|
||||
("Please check a jail for a timing issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
# ignore - too old (obsolete) entry:
|
||||
return []
|
||||
|
||||
|
|
@ -695,11 +720,15 @@ class Filter(JailThread):
|
|||
attempts = self.failManager.addFailure(tick)
|
||||
# avoid RC on busy filter (too many failures) - if attempts for IP/ID reached maxretry,
|
||||
# we can speedup ban, so do it as soon as possible:
|
||||
if self.banASAP and attempts >= self.failManager.getMaxRetry():
|
||||
if attempts >= self.failManager.getMaxRetry():
|
||||
self.performBan(ip)
|
||||
# report to observer - failure was found, for possibly increasing of it retry counter (asynchronous)
|
||||
if Observers.Main is not None:
|
||||
Observers.Main.add('failureFound', self.failManager, self.jail, tick)
|
||||
self.procLines += 1
|
||||
# every 100 lines check need to perform service tasks:
|
||||
if self.procLines % 100 == 0:
|
||||
self.performSvc()
|
||||
# reset (halve) error counter (successfully processed line):
|
||||
if self._errors:
|
||||
self._errors //= 2
|
||||
|
|
@ -709,7 +738,7 @@ class Filter(JailThread):
|
|||
# incr common error counter:
|
||||
self.commonError()
|
||||
|
||||
def commonError(self):
|
||||
def commonError(self, reason="common", exc=None):
|
||||
# incr error counter, stop processing (going idle) after 100th error :
|
||||
self._errors += 1
|
||||
# sleep a little bit (to get around time-related errors):
|
||||
|
|
@ -1002,9 +1031,6 @@ class FileFilter(Filter):
|
|||
log = self.__logs.pop(path)
|
||||
except KeyError:
|
||||
return
|
||||
db = self.jail.database
|
||||
if db is not None:
|
||||
db.updateLog(self.jail, log)
|
||||
logSys.info("Removed logfile: %r", path)
|
||||
self._delLogPath(path)
|
||||
return
|
||||
|
|
@ -1068,6 +1094,7 @@ class FileFilter(Filter):
|
|||
# is created and is added to the FailManager.
|
||||
|
||||
def getFailures(self, filename, inOperation=None):
|
||||
if self.idle: return False
|
||||
log = self.getLog(filename)
|
||||
if log is None:
|
||||
logSys.error("Unable to get failures in %s", filename)
|
||||
|
|
@ -1113,19 +1140,25 @@ class FileFilter(Filter):
|
|||
while not self.idle:
|
||||
line = log.readline()
|
||||
if not self.active: break; # jail has been stopped
|
||||
if not line:
|
||||
if line is None:
|
||||
# The jail reached the bottom, simply set in operation for this log
|
||||
# (since we are first time at end of file, growing is only possible after modifications):
|
||||
log.inOperation = True
|
||||
break
|
||||
# acquire in operation from log and process:
|
||||
self.inOperation = inOperation if inOperation is not None else log.inOperation
|
||||
self.processLineAndAdd(line.rstrip('\r\n'))
|
||||
self.processLineAndAdd(line)
|
||||
finally:
|
||||
log.close()
|
||||
db = self.jail.database
|
||||
if db is not None:
|
||||
db.updateLog(self.jail, log)
|
||||
if self.jail.database is not None:
|
||||
self._pendDBUpdates[log] = 1
|
||||
if (
|
||||
self.ticks % 100 == 0
|
||||
or MyTime.time() >= self._nextUpdateTM
|
||||
or not self.active
|
||||
):
|
||||
self._updateDBPending()
|
||||
self._nextUpdateTM = MyTime.time() + Utils.DEFAULT_SLEEP_TIME * 5
|
||||
return True
|
||||
|
||||
##
|
||||
|
|
@ -1137,6 +1170,8 @@ class FileFilter(Filter):
|
|||
if logSys.getEffectiveLevel() <= logging.DEBUG:
|
||||
logSys.debug("Seek to find time %s (%s), file size %s", date,
|
||||
MyTime.time2str(date), fs)
|
||||
if not fs:
|
||||
return
|
||||
minp = container.getPos()
|
||||
maxp = fs
|
||||
tryPos = minp
|
||||
|
|
@ -1160,8 +1195,8 @@ class FileFilter(Filter):
|
|||
dateTimeMatch = None
|
||||
nextp = None
|
||||
while True:
|
||||
line = container.readline()
|
||||
if not line:
|
||||
line = container.readline(False)
|
||||
if line is None:
|
||||
break
|
||||
(timeMatch, template) = self.dateDetector.matchTime(line)
|
||||
if timeMatch:
|
||||
|
|
@ -1225,12 +1260,33 @@ class FileFilter(Filter):
|
|||
ret.append(("File list", path))
|
||||
return ret
|
||||
|
||||
def stop(self):
|
||||
"""Stop monitoring of log-file(s)
|
||||
def _updateDBPending(self):
|
||||
"""Apply pending updates (log position) to database.
|
||||
"""
|
||||
db = self.jail.database
|
||||
while True:
|
||||
try:
|
||||
log, args = self._pendDBUpdates.popitem()
|
||||
except KeyError:
|
||||
break
|
||||
db.updateLog(self.jail, log)
|
||||
|
||||
def onStop(self):
|
||||
"""Stop monitoring of log-file(s). Invoked after run method.
|
||||
"""
|
||||
# ensure positions of pending logs are up-to-date:
|
||||
if self._pendDBUpdates and self.jail.database:
|
||||
self._updateDBPending()
|
||||
# stop files monitoring:
|
||||
for path in self.__logs.keys():
|
||||
self.delLogPath(path)
|
||||
|
||||
def stop(self):
|
||||
"""Stop filter
|
||||
"""
|
||||
# normally onStop will be called automatically in thread after its run ends,
|
||||
# but for backwards compatibilities we'll invoke it in caller of stop method.
|
||||
self.onStop()
|
||||
# stop thread:
|
||||
super(Filter, self).stop()
|
||||
|
||||
|
|
@ -1258,34 +1314,56 @@ except ImportError: # pragma: no cover
|
|||
|
||||
class FileContainer:
|
||||
|
||||
def __init__(self, filename, encoding, tail=False):
|
||||
def __init__(self, filename, encoding, tail=False, doOpen=False):
|
||||
self.__filename = filename
|
||||
self.waitForLineEnd = True
|
||||
self.setEncoding(encoding)
|
||||
self.__tail = tail
|
||||
self.__handler = None
|
||||
self.__pos = 0
|
||||
self.__pos4hash = 0
|
||||
self.__hash = ''
|
||||
self.__hashNextTime = time.time() + 30
|
||||
# Try to open the file. Raises an exception if an error occurred.
|
||||
handler = open(filename, 'rb')
|
||||
stats = os.fstat(handler.fileno())
|
||||
self.__ino = stats.st_ino
|
||||
if doOpen: # fail2ban-regex only (don't need to reopen it and check for rotation)
|
||||
self.__handler = handler
|
||||
return
|
||||
try:
|
||||
firstLine = handler.readline()
|
||||
# Computes the MD5 of the first line.
|
||||
self.__hash = md5sum(firstLine).hexdigest()
|
||||
# Start at the beginning of file if tail mode is off.
|
||||
if tail:
|
||||
handler.seek(0, 2)
|
||||
self.__pos = handler.tell()
|
||||
else:
|
||||
self.__pos = 0
|
||||
stats = os.fstat(handler.fileno())
|
||||
self.__ino = stats.st_ino
|
||||
if stats.st_size:
|
||||
firstLine = handler.readline()
|
||||
# first line available and contains new-line:
|
||||
if firstLine != firstLine.rstrip(b'\r\n'):
|
||||
# Computes the MD5 of the first line.
|
||||
self.__hash = md5sum(firstLine).hexdigest()
|
||||
# if tail mode scroll to the end of file
|
||||
if tail:
|
||||
handler.seek(0, 2)
|
||||
self.__pos = handler.tell()
|
||||
finally:
|
||||
handler.close()
|
||||
## shows that log is in operation mode (expecting new messages only from here):
|
||||
self.inOperation = tail
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__filename)
|
||||
def __eq__(self, other):
|
||||
return (id(self) == id(other) or
|
||||
self.__filename == (other.__filename if isinstance(other, FileContainer) else other)
|
||||
)
|
||||
def __repr__(self):
|
||||
return 'file-log:'+self.__filename
|
||||
|
||||
def getFileName(self):
|
||||
return self.__filename
|
||||
|
||||
def getFileSize(self):
|
||||
h = self.__handler
|
||||
if h is not None:
|
||||
stats = os.fstat(h.fileno())
|
||||
return stats.st_size
|
||||
return os.path.getsize(self.__filename);
|
||||
|
||||
def setEncoding(self, encoding):
|
||||
|
|
@ -1304,38 +1382,54 @@ class FileContainer:
|
|||
def setPos(self, value):
|
||||
self.__pos = value
|
||||
|
||||
def open(self):
|
||||
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)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
# Stat the file before even attempting to read it
|
||||
stats = os.fstat(self.__handler.fileno())
|
||||
if not stats.st_size:
|
||||
# yoh: so it is still an empty file -- nothing should be
|
||||
# read from it yet
|
||||
# print "D: no content -- return"
|
||||
return False
|
||||
firstLine = self.__handler.readline()
|
||||
# Computes the MD5 of the first line.
|
||||
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.log(logging.MSG, "Log rotation detected for %s", self.__filename)
|
||||
self.__hash = myHash
|
||||
self.__ino = stats.st_ino
|
||||
self.__pos = 0
|
||||
# Sets the file pointer to the last position.
|
||||
self.__handler.seek(self.__pos)
|
||||
def open(self, forcePos=None):
|
||||
h = open(self.__filename, 'rb')
|
||||
try:
|
||||
# Set the file descriptor to be FD_CLOEXEC
|
||||
fd = h.fileno()
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
myHash = self.__hash
|
||||
# Stat the file before even attempting to read it
|
||||
stats = os.fstat(h.fileno())
|
||||
rotflg = stats.st_size < self.__pos or stats.st_ino != self.__ino
|
||||
if rotflg or not len(myHash) or time.time() > self.__hashNextTime:
|
||||
myHash = ''
|
||||
firstLine = h.readline()
|
||||
# Computes the MD5 of the first line (if it is complete)
|
||||
if firstLine != firstLine.rstrip(b'\r\n'):
|
||||
myHash = md5sum(firstLine).hexdigest()
|
||||
self.__hashNextTime = time.time() + 30
|
||||
elif stats.st_size == self.__pos:
|
||||
myHash = self.__hash
|
||||
# Compare size, hash and inode
|
||||
if rotflg or myHash != self.__hash:
|
||||
if self.__hash != '':
|
||||
logSys.log(logging.MSG, "Log rotation detected for %s, reason: %r", self.__filename,
|
||||
(stats.st_size, self.__pos, stats.st_ino, self.__ino, myHash, self.__hash))
|
||||
self.__ino = stats.st_ino
|
||||
self.__pos = 0
|
||||
self.__hash = myHash
|
||||
# if nothing to read from file yet (empty or no new data):
|
||||
if forcePos is not None:
|
||||
self.__pos = forcePos
|
||||
elif stats.st_size <= self.__pos:
|
||||
return False
|
||||
# Sets the file pointer to the last position.
|
||||
h.seek(self.__pos)
|
||||
# leave file open (to read content):
|
||||
self.__handler = h; h = None
|
||||
finally:
|
||||
# close (no content or error only)
|
||||
if h:
|
||||
h.close(); h = None
|
||||
return True
|
||||
|
||||
def seek(self, offs, endLine=True):
|
||||
h = self.__handler
|
||||
if h is None:
|
||||
self.open(offs)
|
||||
h = self.__handler
|
||||
# seek to given position
|
||||
h.seek(offs, 0)
|
||||
# goto end of next line
|
||||
|
|
@ -1353,6 +1447,9 @@ class FileContainer:
|
|||
try:
|
||||
return line.decode(enc, 'strict')
|
||||
except (UnicodeDecodeError, UnicodeEncodeError) as e:
|
||||
# avoid warning if got incomplete end of line (e. g. '\n' in "...[0A" followed by "00]..." for utf-16le:
|
||||
if (e.end == len(line) and line[e.start] in b'\r\n'):
|
||||
return line[0:e.start].decode(enc, 'replace')
|
||||
global _decode_line_warn
|
||||
lev = 7
|
||||
if not _decode_line_warn.get(filename, 0):
|
||||
|
|
@ -1361,29 +1458,85 @@ class FileContainer:
|
|||
logSys.log(lev,
|
||||
"Error decoding line from '%s' with '%s'.", filename, enc)
|
||||
if logSys.getEffectiveLevel() <= lev:
|
||||
logSys.log(lev, "Consider setting logencoding=utf-8 (or another appropriate"
|
||||
" encoding) for this jail. Continuing"
|
||||
" to process line ignoring invalid characters: %r",
|
||||
logSys.log(lev,
|
||||
"Consider setting logencoding to appropriate encoding for this jail. "
|
||||
"Continuing to process line ignoring invalid characters: %r",
|
||||
line)
|
||||
# decode with replacing error chars:
|
||||
line = line.decode(enc, 'replace')
|
||||
return line
|
||||
|
||||
def readline(self):
|
||||
def readline(self, complete=True):
|
||||
"""Read line from file
|
||||
|
||||
In opposite to pythons readline it doesn't return new-line,
|
||||
so returns either the line if line is complete (and complete=True) or None
|
||||
if line is not complete (and complete=True) or there is no content to read.
|
||||
If line is complete (and complete is True), it also shift current known
|
||||
position to begin of next line.
|
||||
|
||||
Also it is safe against interim new-line bytes (e. g. part of multi-byte char)
|
||||
in given encoding.
|
||||
"""
|
||||
if self.__handler is None:
|
||||
return ""
|
||||
return FileContainer.decode_line(
|
||||
self.getFileName(), self.getEncoding(), self.__handler.readline())
|
||||
# read raw bytes up to \n char:
|
||||
b = self.__handler.readline()
|
||||
if not b:
|
||||
return None
|
||||
bl = len(b)
|
||||
# convert to log-encoding (new-line char could disappear if it is part of multi-byte sequence):
|
||||
r = FileContainer.decode_line(
|
||||
self.getFileName(), self.getEncoding(), b)
|
||||
# trim new-line at end and check the line was written complete (contains a new-line):
|
||||
l = r.rstrip('\r\n')
|
||||
if complete:
|
||||
if l == r:
|
||||
# try to fill buffer in order to find line-end in log encoding:
|
||||
fnd = 0
|
||||
while 1:
|
||||
r = self.__handler.readline()
|
||||
if not r:
|
||||
break
|
||||
b += r
|
||||
bl += len(r)
|
||||
# convert to log-encoding:
|
||||
r = FileContainer.decode_line(
|
||||
self.getFileName(), self.getEncoding(), b)
|
||||
# ensure new-line is not in the middle (buffered 2 strings, e. g. in utf-16le it is "...[0A"+"00]..."):
|
||||
e = r.find('\n')
|
||||
if e >= 0 and e != len(r)-1:
|
||||
l, r = r[0:e], r[0:e+1]
|
||||
# back to bytes and get offset to seek after NL:
|
||||
r = r.encode(self.getEncoding(), 'replace')
|
||||
self.__handler.seek(-bl+len(r), 1)
|
||||
return l
|
||||
# trim new-line at end and check the line was written complete (contains a new-line):
|
||||
l = r.rstrip('\r\n')
|
||||
if l != r:
|
||||
return l
|
||||
if self.waitForLineEnd:
|
||||
# not fulfilled - seek back and return:
|
||||
self.__handler.seek(-bl, 1)
|
||||
return None
|
||||
return l
|
||||
|
||||
def close(self):
|
||||
if not self.__handler is None:
|
||||
# Saves the last position.
|
||||
if self.__handler is not None:
|
||||
# Saves the last real position.
|
||||
self.__pos = self.__handler.tell()
|
||||
# Closes the file.
|
||||
self.__handler.close()
|
||||
self.__handler = None
|
||||
## print "D: Closed %s with pos %d" % (handler, self.__pos)
|
||||
## sys.stdout.flush()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
def next(self):
|
||||
line = self.readline()
|
||||
if line is None:
|
||||
self.close()
|
||||
raise StopIteration
|
||||
return line
|
||||
|
||||
_decode_line_warn = Utils.Cache(maxCount=1000, maxTime=24*60*60);
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ class FilterGamin(FileFilter):
|
|||
|
||||
def __init__(self, jail):
|
||||
FileFilter.__init__(self, jail)
|
||||
self.__modified = False
|
||||
# Gamin monitor
|
||||
self.monitor = gamin.WatchMonitor()
|
||||
fd = self.monitor.get_fd()
|
||||
|
|
@ -67,21 +66,9 @@ class FilterGamin(FileFilter):
|
|||
logSys.log(4, "Got event: " + repr(event) + " for " + path)
|
||||
if event in (gamin.GAMCreated, gamin.GAMChanged, gamin.GAMExists):
|
||||
logSys.debug("File changed: " + path)
|
||||
self.__modified = True
|
||||
|
||||
self.ticks += 1
|
||||
self._process_file(path)
|
||||
|
||||
def _process_file(self, path):
|
||||
"""Process a given file
|
||||
|
||||
TODO -- RF:
|
||||
this is a common logic and must be shared/provided by FileFilter
|
||||
"""
|
||||
self.getFailures(path)
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = False
|
||||
|
||||
##
|
||||
# Add a log file path
|
||||
|
|
@ -128,6 +115,9 @@ class FilterGamin(FileFilter):
|
|||
Utils.wait_for(lambda: not self.active or self._handleEvents(),
|
||||
self.sleeptime)
|
||||
self.ticks += 1
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
|
||||
logSys.debug("[%s] filter terminated", self.jailName)
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ __license__ = "GPL"
|
|||
import os
|
||||
import time
|
||||
|
||||
from .failmanager import FailManagerEmpty
|
||||
from .filter import FileFilter
|
||||
from .mytime import MyTime
|
||||
from .utils import Utils
|
||||
from ..helpers import getLogger, logging
|
||||
|
||||
|
|
@ -55,7 +53,6 @@ class FilterPoll(FileFilter):
|
|||
|
||||
def __init__(self, jail):
|
||||
FileFilter.__init__(self, jail)
|
||||
self.__modified = False
|
||||
## The time of the last modification of the file.
|
||||
self.__prevStats = dict()
|
||||
self.__file404Cnt = dict()
|
||||
|
|
@ -115,20 +112,17 @@ class FilterPoll(FileFilter):
|
|||
break
|
||||
for filename in modlst:
|
||||
self.getFailures(filename)
|
||||
self.__modified = True
|
||||
|
||||
self.ticks += 1
|
||||
if self.__modified:
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = False
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
except Exception as e: # pragma: no cover
|
||||
if not self.active: # if not active - error by stop...
|
||||
break
|
||||
logSys.error("Caught unhandled exception in main cycle: %r", e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
# incr common error counter:
|
||||
self.commonError()
|
||||
self.commonError("unhandled", e)
|
||||
logSys.debug("[%s] filter terminated", self.jailName)
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ class FilterPyinotify(FileFilter):
|
|||
|
||||
def __init__(self, jail):
|
||||
FileFilter.__init__(self, jail)
|
||||
self.__modified = False
|
||||
# Pyinotify watch manager
|
||||
self.__monitor = pyinotify.WatchManager()
|
||||
self.__notifier = None
|
||||
|
|
@ -140,9 +139,6 @@ class FilterPyinotify(FileFilter):
|
|||
"""
|
||||
if not self.idle:
|
||||
self.getFailures(path)
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = False
|
||||
|
||||
def _addPending(self, path, reason, isDir=False):
|
||||
if path not in self.__pending:
|
||||
|
|
@ -169,7 +165,7 @@ class FilterPyinotify(FileFilter):
|
|||
return
|
||||
found = {}
|
||||
minTime = 60
|
||||
for path, (retardTM, isDir) in self.__pending.iteritems():
|
||||
for path, (retardTM, isDir) in list(self.__pending.items()):
|
||||
if ntm - self.__pendingChkTime < retardTM:
|
||||
if minTime > retardTM: minTime = retardTM
|
||||
continue
|
||||
|
|
@ -192,7 +188,7 @@ class FilterPyinotify(FileFilter):
|
|||
self._refreshWatcher(path, isDir=isDir)
|
||||
if isDir:
|
||||
# check all files belong to this dir:
|
||||
for logpath in self.__watchFiles:
|
||||
for logpath in list(self.__watchFiles):
|
||||
if logpath.startswith(path + pathsep):
|
||||
# if still no file - add to pending, otherwise refresh and process:
|
||||
if not os.path.isfile(logpath):
|
||||
|
|
@ -272,15 +268,13 @@ class FilterPyinotify(FileFilter):
|
|||
|
||||
def _addLogPath(self, path):
|
||||
self._addFileWatcher(path)
|
||||
# initial scan:
|
||||
# notify (wake up if in waiting):
|
||||
if self.active:
|
||||
# we can execute it right now:
|
||||
self._process_file(path)
|
||||
else:
|
||||
# retard until filter gets started, isDir=None signals special case: process file only (don't need to refresh monitor):
|
||||
self._addPending(path, ('INITIAL', path), isDir=None)
|
||||
self.__pendingMinTime = 0
|
||||
# retard until filter gets started, isDir=None signals special case: process file only (don't need to refresh monitor):
|
||||
self._addPending(path, ('INITIAL', path), isDir=None)
|
||||
|
||||
##
|
||||
##
|
||||
# Delete a log path
|
||||
#
|
||||
# @param path the log file to delete
|
||||
|
|
@ -291,7 +285,7 @@ class FilterPyinotify(FileFilter):
|
|||
logSys.error("Failed to remove watch on path: %s", path)
|
||||
|
||||
path_dir = dirname(path)
|
||||
for k in self.__watchFiles:
|
||||
for k in list(self.__watchFiles):
|
||||
if k.startswith(path_dir + pathsep):
|
||||
path_dir = None
|
||||
break
|
||||
|
|
@ -345,16 +339,26 @@ class FilterPyinotify(FileFilter):
|
|||
self.__notifier.process_events()
|
||||
|
||||
# wait for events / timeout:
|
||||
notify_maxtout = self.__notify_maxtout
|
||||
def __check_events():
|
||||
return not self.active or self.__notifier.check_events(timeout=notify_maxtout)
|
||||
if Utils.wait_for(__check_events, min(self.sleeptime, self.__pendingMinTime)):
|
||||
return (
|
||||
not self.active
|
||||
or bool(self.__notifier.check_events(timeout=self.__notify_maxtout))
|
||||
or (self.__pendingMinTime and self.__pending)
|
||||
)
|
||||
wres = Utils.wait_for(__check_events, min(self.sleeptime, self.__pendingMinTime))
|
||||
if wres:
|
||||
if not self.active: break
|
||||
self.__notifier.read_events()
|
||||
if not isinstance(wres, dict):
|
||||
self.__notifier.read_events()
|
||||
|
||||
self.ticks += 1
|
||||
|
||||
# check pending files/dirs (logrotate ready):
|
||||
if not self.idle:
|
||||
self._checkPending()
|
||||
if self.idle:
|
||||
continue
|
||||
self._checkPending()
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
if not self.active: # if not active - error by stop...
|
||||
|
|
@ -362,10 +366,8 @@ class FilterPyinotify(FileFilter):
|
|||
logSys.error("Caught unhandled exception in main cycle: %r", e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
# incr common error counter:
|
||||
self.commonError()
|
||||
self.commonError("unhandled", e)
|
||||
|
||||
self.ticks += 1
|
||||
|
||||
logSys.debug("[%s] filter exited (pyinotifier)", self.jailName)
|
||||
self.__notifier = None
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ __copyright__ = "Copyright (c) 2013 Steven Hiscocks"
|
|||
__license__ = "GPL"
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
|
|
@ -91,9 +92,14 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
|||
try:
|
||||
args['flags'] = int(kwargs.pop('journalflags'))
|
||||
except KeyError:
|
||||
# be sure all journal types will be opened if files specified (don't set flags):
|
||||
if 'files' not in args or not len(args['files']):
|
||||
args['flags'] = 4
|
||||
# be sure all journal types will be opened if files/path specified (don't set flags):
|
||||
if ('files' not in args or not len(args['files'])) and ('path' not in args or not args['path']):
|
||||
args['flags'] = int(os.getenv("F2B_SYSTEMD_DEFAULT_FLAGS", 4))
|
||||
|
||||
try:
|
||||
args['namespace'] = kwargs.pop('namespace')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return args
|
||||
|
||||
|
|
@ -252,6 +258,10 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
|||
date = datetime.datetime.fromtimestamp(date)
|
||||
self.__journal.seek_realtime(date)
|
||||
|
||||
def inOperationMode(self):
|
||||
self.inOperation = True
|
||||
logSys.info("[%s] Jail is in operation now (process new journal entries)", self.jailName)
|
||||
|
||||
##
|
||||
# Main loop.
|
||||
#
|
||||
|
|
@ -262,24 +272,46 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
|||
|
||||
if not self.getJournalMatch():
|
||||
logSys.notice(
|
||||
"Jail started without 'journalmatch' set. "
|
||||
"[%s] Jail started without 'journalmatch' set. "
|
||||
"Jail regexs will be checked against all journal entries, "
|
||||
"which is not advised for performance reasons.")
|
||||
"which is not advised for performance reasons.", self.jailName)
|
||||
|
||||
# Try to obtain the last known time (position of journal)
|
||||
start_time = 0
|
||||
if self.jail.database is not None:
|
||||
start_time = self.jail.database.getJournalPos(self.jail, 'systemd-journal') or 0
|
||||
# Seek to max(last_known_time, now - findtime) in journal
|
||||
start_time = max( start_time, MyTime.time() - int(self.getFindTime()) )
|
||||
self.seekToTime(start_time)
|
||||
# Move back one entry to ensure do not end up in dead space
|
||||
# if start time beyond end of journal
|
||||
# Save current cursor position (to recognize in operation mode):
|
||||
logentry = None
|
||||
try:
|
||||
self.__journal.get_previous()
|
||||
self.__journal.seek_tail()
|
||||
logentry = self.__journal.get_previous()
|
||||
self.__journal.get_next()
|
||||
except OSError:
|
||||
pass # Reading failure, so safe to ignore
|
||||
logentry = None # Reading failure, so safe to ignore
|
||||
if logentry:
|
||||
# Try to obtain the last known time (position of journal)
|
||||
startTime = 0
|
||||
if self.jail.database is not None:
|
||||
startTime = self.jail.database.getJournalPos(self.jail, 'systemd-journal') or 0
|
||||
# Seek to max(last_known_time, now - findtime) in journal
|
||||
startTime = max( startTime, MyTime.time() - int(self.getFindTime()) )
|
||||
self.seekToTime(startTime)
|
||||
# Not in operation while we'll read old messages ...
|
||||
self.inOperation = False
|
||||
# Save current time in order to check time to switch "in operation" mode
|
||||
startTime = (1, MyTime.time(), logentry.get('__CURSOR'))
|
||||
# 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
|
||||
else:
|
||||
# empty journal or no entries for current filter:
|
||||
self.inOperationMode()
|
||||
# seek_tail() seems to have a bug by no entries (could bypass some entries hereafter), so seek to now instead:
|
||||
startTime = MyTime.time()
|
||||
self.seekToTime(startTime)
|
||||
# for possible future switches of in-operation mode:
|
||||
startTime = (0, startTime)
|
||||
|
||||
line = None
|
||||
while self.active:
|
||||
# wait for records (or for timeout in sleeptime seconds):
|
||||
try:
|
||||
|
|
@ -289,9 +321,10 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
|||
#self.__journal.wait(self.sleeptime) != journal.NOP
|
||||
##
|
||||
## wait for entries without sleep in intervals, because "sleeping" in journal.wait:
|
||||
Utils.wait_for(lambda: not self.active or \
|
||||
self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP,
|
||||
self.sleeptime, 0.00001)
|
||||
if not logentry:
|
||||
Utils.wait_for(lambda: not self.active or \
|
||||
self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP,
|
||||
self.sleeptime, 0.00001)
|
||||
if self.idle:
|
||||
# because journal.wait will returns immediatelly if we have records in journal,
|
||||
# just wait a little bit here for not idle, to prevent hi-load:
|
||||
|
|
@ -310,27 +343,50 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
|||
e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG)
|
||||
self.ticks += 1
|
||||
if logentry:
|
||||
line = self.formatJournalEntry(logentry)
|
||||
self.processLineAndAdd(*line)
|
||||
line, tm = self.formatJournalEntry(logentry)
|
||||
# switch "in operation" mode if we'll find start entry (+ some delta):
|
||||
if not self.inOperation:
|
||||
if tm >= MyTime.time() - 1: # reached now (approximated):
|
||||
self.inOperationMode()
|
||||
elif startTime[0] == 1:
|
||||
# if it reached start entry (or get read time larger than start time)
|
||||
if logentry.get('__CURSOR') == startTime[2] or tm > startTime[1]:
|
||||
# give the filter same time it needed to reach the start entry:
|
||||
startTime = (0, MyTime.time()*2 - startTime[1])
|
||||
elif tm > startTime[1]: # reached start time (approximated):
|
||||
self.inOperationMode()
|
||||
# process line
|
||||
self.processLineAndAdd(line, tm)
|
||||
self.__modified += 1
|
||||
if self.__modified >= 100: # todo: should be configurable
|
||||
break
|
||||
else:
|
||||
# "in operation" mode since we don't have messages anymore (reached end of journal):
|
||||
if not self.inOperation:
|
||||
self.inOperationMode()
|
||||
break
|
||||
if self.__modified:
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = 0
|
||||
# update position in log (time and iso string):
|
||||
if self.jail.database is not None:
|
||||
self.jail.database.updateJournal(self.jail, 'systemd-journal', line[1], line[0][1])
|
||||
self.__modified = 0
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
# update position in log (time and iso string):
|
||||
if self.jail.database:
|
||||
if line:
|
||||
self._pendDBUpdates['systemd-journal'] = (tm, line[1])
|
||||
line = None
|
||||
if self._pendDBUpdates and (
|
||||
self.ticks % 100 == 0
|
||||
or MyTime.time() >= self._nextUpdateTM
|
||||
or not self.active
|
||||
):
|
||||
self._updateDBPending()
|
||||
self._nextUpdateTM = MyTime.time() + Utils.DEFAULT_SLEEP_TIME * 5
|
||||
except Exception as e: # pragma: no cover
|
||||
if not self.active: # if not active - error by stop...
|
||||
break
|
||||
logSys.error("Caught unhandled exception in main cycle: %r", e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
# incr common error counter:
|
||||
self.commonError()
|
||||
self.commonError("unhandled", e)
|
||||
|
||||
logSys.debug("[%s] filter terminated", self.jailName)
|
||||
|
||||
|
|
@ -350,3 +406,22 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
|||
ret.append(("Journal matches",
|
||||
[" + ".join(" ".join(match) for match in self.__matches)]))
|
||||
return ret
|
||||
|
||||
def _updateDBPending(self):
|
||||
"""Apply pending updates (jornal position) to database.
|
||||
"""
|
||||
db = self.jail.database
|
||||
while True:
|
||||
try:
|
||||
log, args = self._pendDBUpdates.popitem()
|
||||
except KeyError:
|
||||
break
|
||||
db.updateJournal(self.jail, log, *args)
|
||||
|
||||
def onStop(self):
|
||||
"""Stop monitoring of journal. Invoked after run method.
|
||||
"""
|
||||
# ensure positions of pending logs are up-to-date:
|
||||
if self._pendDBUpdates and self.jail.database:
|
||||
self._updateDBPending()
|
||||
|
||||
|
|
|
|||
|
|
@ -169,27 +169,31 @@ class DNSUtils:
|
|||
DNSUtils.CACHE_ipToName.set(key, name)
|
||||
return name
|
||||
|
||||
# key find cached own hostnames (this tuple-key cannot be used elsewhere):
|
||||
_getSelfNames_key = ('self','dns')
|
||||
|
||||
@staticmethod
|
||||
def getSelfNames():
|
||||
"""Get own host names of self"""
|
||||
# try find cached own hostnames (this tuple-key cannot be used elsewhere):
|
||||
key = ('self','dns')
|
||||
names = DNSUtils.CACHE_ipToName.get(key)
|
||||
# try find cached own hostnames:
|
||||
names = DNSUtils.CACHE_ipToName.get(DNSUtils._getSelfNames_key)
|
||||
# get it using different ways (a set with names of localhost, hostname, fully qualified):
|
||||
if names is None:
|
||||
names = set([
|
||||
'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True)
|
||||
]) - set(['']) # getHostname can return ''
|
||||
# cache and return :
|
||||
DNSUtils.CACHE_ipToName.set(key, names)
|
||||
DNSUtils.CACHE_ipToName.set(DNSUtils._getSelfNames_key, names)
|
||||
return names
|
||||
|
||||
# key to find cached own IPs (this tuple-key cannot be used elsewhere):
|
||||
_getSelfIPs_key = ('self','ips')
|
||||
|
||||
@staticmethod
|
||||
def getSelfIPs():
|
||||
"""Get own IP addresses of self"""
|
||||
# try find cached own IPs (this tuple-key cannot be used elsewhere):
|
||||
key = ('self','ips')
|
||||
ips = DNSUtils.CACHE_nameToIp.get(key)
|
||||
# to find cached own IPs:
|
||||
ips = DNSUtils.CACHE_nameToIp.get(DNSUtils._getSelfIPs_key)
|
||||
# get it using different ways (a set with IPs of localhost, hostname, fully qualified):
|
||||
if ips is None:
|
||||
ips = set()
|
||||
|
|
@ -199,13 +203,30 @@ class DNSUtils:
|
|||
except Exception as e: # pragma: no cover
|
||||
logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e)
|
||||
# cache and return :
|
||||
DNSUtils.CACHE_nameToIp.set(key, ips)
|
||||
DNSUtils.CACHE_nameToIp.set(DNSUtils._getSelfIPs_key, ips)
|
||||
return ips
|
||||
|
||||
_IPv6IsAllowed = None
|
||||
|
||||
@staticmethod
|
||||
def setIPv6IsAllowed(value):
|
||||
DNSUtils._IPv6IsAllowed = value
|
||||
logSys.debug("IPv6 is %s", ('on' if value else 'off') if value is not None else 'auto')
|
||||
return value
|
||||
|
||||
# key to find cached value of IPv6 allowance (this tuple-key cannot be used elsewhere):
|
||||
_IPv6IsAllowed_key = ('self','ipv6-allowed')
|
||||
|
||||
@staticmethod
|
||||
def IPv6IsAllowed():
|
||||
# return os.path.exists("/proc/net/if_inet6") || any((':' in ip) for ip in DNSUtils.getSelfIPs())
|
||||
return any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
|
||||
if DNSUtils._IPv6IsAllowed is not None:
|
||||
return DNSUtils._IPv6IsAllowed
|
||||
v = DNSUtils.CACHE_nameToIp.get(DNSUtils._IPv6IsAllowed_key)
|
||||
if v is not None:
|
||||
return v
|
||||
v = any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
|
||||
DNSUtils.CACHE_nameToIp.set(DNSUtils._IPv6IsAllowed_key, v)
|
||||
return v
|
||||
|
||||
|
||||
##
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko"
|
|||
__license__ = "GPL"
|
||||
|
||||
from threading import Lock
|
||||
from collections import Mapping
|
||||
try:
|
||||
from collections.abc import Mapping
|
||||
except ImportError:
|
||||
from collections import Mapping
|
||||
|
||||
from ..exceptions import DuplicateJailException, UnknownJailException
|
||||
from .jail import Jail
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ class JailThread(Thread):
|
|||
def run_with_except_hook(*args, **kwargs):
|
||||
try:
|
||||
run(*args, **kwargs)
|
||||
# call on stop callback to do some finalizations:
|
||||
self.onStop()
|
||||
except Exception as e:
|
||||
# avoid very sporadic error "'NoneType' object has no attribute 'exc_info'" (https://bugs.python.org/issue7336)
|
||||
# only extremely fast systems are affected ATM (2.7 / 3.x), if thread ends nothing is available here.
|
||||
|
|
@ -97,6 +99,12 @@ class JailThread(Thread):
|
|||
self.active = True
|
||||
super(JailThread, self).start()
|
||||
|
||||
@abstractmethod
|
||||
def onStop(self): # pragma: no cover - absract
|
||||
"""Abstract - Called when thread ends (after run).
|
||||
"""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""Sets `active` property to False, to flag run method to return.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -174,3 +174,62 @@ class MyTime:
|
|||
val = rexp.sub(rpl, val)
|
||||
val = MyTime._str2sec_fini.sub(r"\1+\2", val)
|
||||
return eval(val)
|
||||
|
||||
class seconds2str():
|
||||
"""Converts seconds to string on demand (if string representation needed).
|
||||
Ex: seconds2str(86400*390) = 1y 3w 4d
|
||||
seconds2str(86400*368) = 1y 3d
|
||||
seconds2str(86400*365.5) = 1y
|
||||
seconds2str(86400*2+3600*7+60*15) = 2d 7h 15m
|
||||
seconds2str(86400*2+3599) = 2d 1h
|
||||
seconds2str(3600-5) = 1h
|
||||
seconds2str(3600-10) = 59m 50s
|
||||
seconds2str(59) = 59s
|
||||
"""
|
||||
def __init__(self, sec):
|
||||
self.sec = sec
|
||||
def __str__(self):
|
||||
# s = str(datetime.timedelta(seconds=int(self.sec)))
|
||||
# return s if s[-3:] != ":00" else s[:-3]
|
||||
s = self.sec; c = 3
|
||||
# automatic accuracy: round by large values (and maximally 3 groups)
|
||||
if s >= 31536000: # a year as 365*24*60*60 (don't need to consider leap year by this accuracy)
|
||||
s = int(round(float(s)/86400)) # round by a day
|
||||
r = str(s//365) + 'y '; s %= 365
|
||||
if s >= 7:
|
||||
r += str(s//7) + 'w '; s %= 7
|
||||
if s:
|
||||
r += str(s) + 'd '
|
||||
return r[:-1]
|
||||
if s >= 604800: # a week as 24*60*60*7
|
||||
s = int(round(float(s)/3600)) # round by a hour
|
||||
r = str(s//168) + 'w '; s %= 168
|
||||
if s >= 24:
|
||||
r += str(s//24) + 'd '; s %= 24
|
||||
if s:
|
||||
r += str(s) + 'h '
|
||||
return r[:-1]
|
||||
if s >= 86400: # a day as 24*60*60
|
||||
s = int(round(float(s)/60)) # round by a minute
|
||||
r = str(s//1440) + 'd '; s %= 1440
|
||||
if s >= 60:
|
||||
r += str(s//60) + 'h '; s %= 60
|
||||
if s:
|
||||
r += str(s) + 'm '
|
||||
return r[:-1]
|
||||
if s >= 3595: # a hour as 60*60 (- 5 seconds)
|
||||
s = int(round(float(s)/10)) # round by 10 seconds
|
||||
r = str(s//360) + 'h '; s %= 360
|
||||
if s >= 6: # a minute
|
||||
r += str(s//6) + 'm '; s %= 6
|
||||
return r[:-1]
|
||||
r = ''
|
||||
if s >= 60: # a minute
|
||||
r += str(s//60) + 'm '; s %= 60
|
||||
if s: # remaining seconds
|
||||
r += str(s) + 's '
|
||||
elif not self.sec: # 0s
|
||||
r = '0 '
|
||||
return r[:-1]
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ class ObserverThread(JailThread):
|
|||
if ticket.getTime() > timeOfBan:
|
||||
logSys.info('[%s] IP %s is bad: %s # last %s - incr %s to %s' % (jail.name, ip, banCount,
|
||||
MyTime.time2str(timeOfBan),
|
||||
datetime.timedelta(seconds=int(orgBanTime)), datetime.timedelta(seconds=int(banTime))));
|
||||
MyTime.seconds2str(orgBanTime), MyTime.seconds2str(banTime)))
|
||||
else:
|
||||
ticket.restored = True
|
||||
break
|
||||
|
|
@ -491,8 +491,7 @@ class ObserverThread(JailThread):
|
|||
# if not permanent
|
||||
if btime != -1:
|
||||
bendtime = ticket.getTime() + btime
|
||||
logtime = (datetime.timedelta(seconds=int(btime)),
|
||||
MyTime.time2str(bendtime))
|
||||
logtime = (MyTime.seconds2str(btime), MyTime.time2str(bendtime))
|
||||
# check ban is not too old :
|
||||
if bendtime < MyTime.time():
|
||||
logSys.debug('Ignore old bantime %s', logtime[1])
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import sys
|
|||
|
||||
from .observer import Observers, ObserverThread
|
||||
from .jails import Jails
|
||||
from .filter import FileFilter, JournalFilter
|
||||
from .filter import DNSUtils, FileFilter, JournalFilter
|
||||
from .transmitter import Transmitter
|
||||
from .asyncserver import AsyncServer, AsyncServerException
|
||||
from .. import version
|
||||
|
|
@ -293,6 +293,11 @@ class Server:
|
|||
for name in self.__jails.keys():
|
||||
self.delJail(name, stop=False, join=True)
|
||||
|
||||
def clearCaches(self):
|
||||
# we need to clear caches, to be able to recognize new IPs/families etc:
|
||||
DNSUtils.CACHE_nameToIp.clear()
|
||||
DNSUtils.CACHE_ipToName.clear()
|
||||
|
||||
def reloadJails(self, name, opts, begin):
|
||||
if begin:
|
||||
# begin reload:
|
||||
|
|
@ -314,6 +319,8 @@ class Server:
|
|||
if "--restart" in opts:
|
||||
self.stopJail(name)
|
||||
else:
|
||||
# invalidate caches by reload
|
||||
self.clearCaches()
|
||||
# first unban all ips (will be not restored after (re)start):
|
||||
if "--unban" in opts:
|
||||
self.setUnbanIP()
|
||||
|
|
@ -803,6 +810,11 @@ class Server:
|
|||
logSys.info("flush performed on %s" % self.__logTarget)
|
||||
return "flushed"
|
||||
|
||||
@staticmethod
|
||||
def setIPv6IsAllowed(value):
|
||||
value = _as_bool(value) if value != 'auto' else None
|
||||
return DNSUtils.setIPv6IsAllowed(value)
|
||||
|
||||
def setThreadOptions(self, value):
|
||||
for o, v in value.iteritems():
|
||||
if o == 'stacksize':
|
||||
|
|
|
|||
|
|
@ -30,17 +30,6 @@ locale_time = LocaleTime()
|
|||
TZ_ABBR_RE = r"[A-Z](?:[A-Z]{2,4})?"
|
||||
FIXED_OFFSET_TZ_RE = re.compile(r"(%s)?([+-][01]\d(?::?\d{2})?)?$" % (TZ_ABBR_RE,))
|
||||
|
||||
def _getYearCentRE(cent=(0,3), distance=3, now=(MyTime.now(), MyTime.alternateNow)):
|
||||
""" Build century regex for last year and the next years (distance).
|
||||
|
||||
Thereby respect possible run in the test-cases (alternate date used there)
|
||||
"""
|
||||
cent = lambda year, f=cent[0], t=cent[1]: str(year)[f:t]
|
||||
exprset = set( cent(now[0].year + i) for i in (-1, distance) )
|
||||
if len(now) and now[1]:
|
||||
exprset |= set( cent(now[1].year + i) for i in (-1, distance) )
|
||||
return "(?:%s)" % "|".join(exprset) if len(exprset) > 1 else "".join(exprset)
|
||||
|
||||
timeRE = TimeRE()
|
||||
|
||||
# %k - one- or two-digit number giving the hour of the day (0-23) on a 24-hour clock,
|
||||
|
|
@ -63,20 +52,68 @@ timeRE['z'] = r"(?P<z>Z|UTC|GMT|[+-][01]\d(?::?\d{2})?)"
|
|||
timeRE['ExZ'] = r"(?P<Z>%s)" % (TZ_ABBR_RE,)
|
||||
timeRE['Exz'] = r"(?P<z>(?:%s)?[+-][01]\d(?::?\d{2})?|%s)" % (TZ_ABBR_RE, TZ_ABBR_RE)
|
||||
|
||||
# overwrite default patterns, since they can be non-optimal:
|
||||
timeRE['d'] = r"(?P<d>[1-2]\d|[0 ]?[1-9]|3[0-1])"
|
||||
timeRE['m'] = r"(?P<m>0?[1-9]|1[0-2])"
|
||||
timeRE['Y'] = r"(?P<Y>\d{4})"
|
||||
timeRE['H'] = r"(?P<H>[0-1]?\d|2[0-3])"
|
||||
timeRE['M'] = r"(?P<M>[0-5]?\d)"
|
||||
timeRE['S'] = r"(?P<S>[0-5]?\d|6[0-1])"
|
||||
|
||||
# Extend build-in TimeRE with some exact patterns
|
||||
# exact two-digit patterns:
|
||||
timeRE['Exd'] = r"(?P<d>3[0-1]|[1-2]\d|0[1-9])"
|
||||
timeRE['Exm'] = r"(?P<m>1[0-2]|0[1-9])"
|
||||
timeRE['ExH'] = r"(?P<H>2[0-3]|[0-1]\d)"
|
||||
timeRE['Exk'] = r" ?(?P<H>2[0-3]|[0-1]\d|\d)"
|
||||
timeRE['Exd'] = r"(?P<d>[1-2]\d|0[1-9]|3[0-1])"
|
||||
timeRE['Exm'] = r"(?P<m>0[1-9]|1[0-2])"
|
||||
timeRE['ExH'] = r"(?P<H>[0-1]\d|2[0-3])"
|
||||
timeRE['Exk'] = r" ?(?P<H>[0-1]?\d|2[0-3])"
|
||||
timeRE['Exl'] = r" ?(?P<I>1[0-2]|\d)"
|
||||
timeRE['ExM'] = r"(?P<M>[0-5]\d)"
|
||||
timeRE['ExS'] = r"(?P<S>6[0-1]|[0-5]\d)"
|
||||
# more precise year patterns, within same century of last year and
|
||||
# the next 3 years (for possible long uptime of fail2ban); thereby
|
||||
# respect possible run in the test-cases (alternate date used there):
|
||||
timeRE['ExY'] = r"(?P<Y>%s\d)" % _getYearCentRE(cent=(0,3), distance=3)
|
||||
timeRE['Exy'] = r"(?P<y>%s\d)" % _getYearCentRE(cent=(2,3), distance=3)
|
||||
timeRE['ExS'] = r"(?P<S>[0-5]\d|6[0-1])"
|
||||
|
||||
def _updateTimeRE():
|
||||
def _getYearCentRE(cent=(0,3), distance=3, now=(MyTime.now(), MyTime.alternateNow)):
|
||||
""" Build century regex for last year and the next years (distance).
|
||||
|
||||
Thereby respect possible run in the test-cases (alternate date used there)
|
||||
"""
|
||||
cent = lambda year, f=cent[0], t=cent[1]: str(year)[f:t]
|
||||
def grp(exprset):
|
||||
c = None
|
||||
if len(exprset) > 1:
|
||||
for i in exprset:
|
||||
if c is None or i[0:-1] == c:
|
||||
c = i[0:-1]
|
||||
else:
|
||||
c = None
|
||||
break
|
||||
if not c:
|
||||
for i in exprset:
|
||||
if c is None or i[0] == c:
|
||||
c = i[0]
|
||||
else:
|
||||
c = None
|
||||
break
|
||||
if c:
|
||||
return "%s%s" % (c, grp([i[len(c):] for i in exprset]))
|
||||
return ("(?:%s)" % "|".join(exprset) if len(exprset[0]) > 1 else "[%s]" % "".join(exprset)) \
|
||||
if len(exprset) > 1 else "".join(exprset)
|
||||
exprset = set( cent(now[0].year + i) for i in (-1, distance) )
|
||||
if len(now) > 1 and now[1]:
|
||||
exprset |= set( cent(now[1].year + i) for i in xrange(-1, now[0].year-now[1].year+1, distance) )
|
||||
return grp(sorted(list(exprset)))
|
||||
|
||||
# more precise year patterns, within same century of last year and
|
||||
# the next 3 years (for possible long uptime of fail2ban); thereby
|
||||
# consider possible run in the test-cases (alternate date used there),
|
||||
# so accept years: 20xx (from test-date or 2001 up to current century)
|
||||
timeRE['ExY'] = r"(?P<Y>%s\d)" % _getYearCentRE(cent=(0,3), distance=3,
|
||||
now=(datetime.datetime.now(), datetime.datetime.fromtimestamp(
|
||||
min(MyTime.alternateNowTime or 978393600, 978393600))
|
||||
)
|
||||
)
|
||||
timeRE['Exy'] = r"(?P<y>\d{2})"
|
||||
|
||||
_updateTimeRE()
|
||||
|
||||
def getTimePatternRE():
|
||||
keys = timeRE.keys()
|
||||
|
|
@ -168,9 +205,9 @@ def reGroupDictStrptime(found_dict, msec=False, default_tz=None):
|
|||
"""
|
||||
|
||||
now = \
|
||||
year = month = day = hour = minute = tzoffset = \
|
||||
year = month = day = tzoffset = \
|
||||
weekday = julian = week_of_year = None
|
||||
second = fraction = 0
|
||||
hour = minute = second = fraction = 0
|
||||
for key, val in found_dict.iteritems():
|
||||
if val is None: continue
|
||||
# Directives not explicitly handled below:
|
||||
|
|
|
|||
|
|
@ -173,6 +173,11 @@ class Transmitter:
|
|||
return self.__server.getSyslogSocket()
|
||||
else:
|
||||
raise Exception("Failed to change syslog socket")
|
||||
elif name == "allowipv6":
|
||||
value = command[1]
|
||||
self.__server.setIPv6IsAllowed(value)
|
||||
if self.__quiet: return
|
||||
return value
|
||||
#Thread
|
||||
elif name == "thread":
|
||||
value = command[1]
|
||||
|
|
|
|||
|
|
@ -332,11 +332,9 @@ class Utils():
|
|||
timeout_expr = lambda: time.time() > time0
|
||||
else:
|
||||
timeout_expr = timeout
|
||||
if not interval:
|
||||
interval = Utils.DEFAULT_SLEEP_INTERVAL
|
||||
if timeout_expr():
|
||||
break
|
||||
stm = min(stm + interval, Utils.DEFAULT_SLEEP_TIME)
|
||||
stm = min(stm + (interval or Utils.DEFAULT_SLEEP_INTERVAL), Utils.DEFAULT_SLEEP_TIME)
|
||||
time.sleep(stm)
|
||||
return ret
|
||||
|
||||
|
|
|
|||
|
|
@ -1,157 +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.
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import sys
|
||||
from functools import wraps
|
||||
from socket import timeout
|
||||
from ssl import SSLError
|
||||
|
||||
from ..actiontestcase import CallingMap
|
||||
from ..dummyjail import DummyJail
|
||||
from ..servertestcase import IPAddr
|
||||
from ..utils import LogCaptureTestCase, CONFIG_DIR
|
||||
|
||||
if sys.version_info >= (3, ): # pragma: 2.x no cover
|
||||
from urllib.error import HTTPError, URLError
|
||||
else: # pragma: 3.x no cover
|
||||
from urllib2 import HTTPError, URLError
|
||||
|
||||
def skip_if_not_available(f):
|
||||
"""Helper to decorate tests to skip in case of timeout/http-errors like "502 bad gateway".
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapper(self, *args):
|
||||
try:
|
||||
return f(self, *args)
|
||||
except (SSLError, HTTPError, URLError, timeout) as e: # pragma: no cover - timeout/availability issues
|
||||
if not isinstance(e, timeout) and 'timed out' not in str(e):
|
||||
if not hasattr(e, 'code') or e.code > 200 and e.code <= 404:
|
||||
raise
|
||||
raise unittest.SkipTest('Skip test because of %s' % e)
|
||||
return wrapper
|
||||
|
||||
if sys.version_info >= (2,7): # pragma: no cover - may be unavailable
|
||||
class BadIPsActionTest(LogCaptureTestCase):
|
||||
|
||||
available = True, None
|
||||
pythonModule = None
|
||||
modAction = None
|
||||
|
||||
@skip_if_not_available
|
||||
def setUp(self):
|
||||
"""Call before every test case."""
|
||||
super(BadIPsActionTest, self).setUp()
|
||||
unittest.F2B.SkipIfNoNetwork()
|
||||
|
||||
self.jail = DummyJail()
|
||||
|
||||
self.jail.actions.add("test")
|
||||
|
||||
pythonModuleName = os.path.join(CONFIG_DIR, "action.d", "badips.py")
|
||||
|
||||
# check availability (once if not alive, used shorter timeout as in test cases):
|
||||
if BadIPsActionTest.available[0]:
|
||||
if not BadIPsActionTest.modAction:
|
||||
if not BadIPsActionTest.pythonModule:
|
||||
BadIPsActionTest.pythonModule = self.jail.actions._load_python_module(pythonModuleName)
|
||||
BadIPsActionTest.modAction = BadIPsActionTest.pythonModule.Action
|
||||
self.jail.actions._load_python_module(pythonModuleName)
|
||||
BadIPsActionTest.available = BadIPsActionTest.modAction.isAvailable(timeout=2 if unittest.F2B.fast else 30)
|
||||
if not BadIPsActionTest.available[0]:
|
||||
raise unittest.SkipTest('Skip test because service is not available: %s' % BadIPsActionTest.available[1])
|
||||
|
||||
self.jail.actions.add("badips", pythonModuleName, initOpts={
|
||||
'category': "ssh",
|
||||
'banaction': "test",
|
||||
'age': "2w",
|
||||
'score': 5,
|
||||
#'key': "fail2ban-test-suite",
|
||||
#'bankey': "fail2ban-test-suite",
|
||||
'timeout': (3 if unittest.F2B.fast else 60),
|
||||
})
|
||||
self.action = self.jail.actions["badips"]
|
||||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
# Must cancel timer!
|
||||
if self.action._timer:
|
||||
self.action._timer.cancel()
|
||||
super(BadIPsActionTest, self).tearDown()
|
||||
|
||||
@skip_if_not_available
|
||||
def testCategory(self):
|
||||
categories = self.action.getCategories()
|
||||
self.assertIn("ssh", categories)
|
||||
self.assertTrue(len(categories) >= 10)
|
||||
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "category",
|
||||
"invalid-category")
|
||||
|
||||
# Not valid for reporting category...
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "category", "mail")
|
||||
# but valid for blacklisting.
|
||||
self.action.bancategory = "mail"
|
||||
|
||||
@skip_if_not_available
|
||||
def testScore(self):
|
||||
self.assertRaises(ValueError, setattr, self.action, "score", -5)
|
||||
self.action.score = 3
|
||||
self.action.score = "3"
|
||||
|
||||
@skip_if_not_available
|
||||
def testBanaction(self):
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "banaction",
|
||||
"invalid-action")
|
||||
self.action.banaction = "test"
|
||||
|
||||
@skip_if_not_available
|
||||
def testUpdateperiod(self):
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "updateperiod", -50)
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "updateperiod", 0)
|
||||
self.action.updateperiod = 900
|
||||
self.action.updateperiod = "900"
|
||||
|
||||
@skip_if_not_available
|
||||
def testStartStop(self):
|
||||
self.action.start()
|
||||
self.assertTrue(len(self.action._bannedips) > 10,
|
||||
"%s is fewer as 10: %r" % (len(self.action._bannedips), self.action._bannedips))
|
||||
self.action.stop()
|
||||
self.assertTrue(len(self.action._bannedips) == 0)
|
||||
|
||||
@skip_if_not_available
|
||||
def testBanIP(self):
|
||||
aInfo = CallingMap({
|
||||
'ip': IPAddr('192.0.2.1')
|
||||
})
|
||||
self.action.ban(aInfo)
|
||||
self.assertLogged('badips.com: ban', wait=True)
|
||||
self.pruneLog()
|
||||
# produce an error using wrong category/IP:
|
||||
self.action._category = 'f2b-this-category-dont-available-test-suite-only'
|
||||
aInfo['ip'] = ''
|
||||
self.assertRaises(BadIPsActionTest.pythonModule.HTTPError, self.action.ban, aInfo)
|
||||
self.assertLogged('IP is invalid', 'invalid category', wait=True, all=False)
|
||||
|
|
@ -381,13 +381,16 @@ class JailReaderTest(LogCaptureTestCase):
|
|||
self.assertEqual(('mail.who_is', {'a':'cat', 'b':'dog'}), extractOptions("mail.who_is[a=cat,b=dog]"))
|
||||
self.assertEqual(('mail--ho_is', {}), extractOptions("mail--ho_is"))
|
||||
|
||||
self.assertEqual(('mail--ho_is', {}), extractOptions("mail--ho_is['s']"))
|
||||
#print(self.getLog())
|
||||
#self.assertLogged("Invalid argument ['s'] in ''s''")
|
||||
|
||||
self.assertEqual(('mail', {'a': ','}), extractOptions("mail[a=',']"))
|
||||
self.assertEqual(('mail', {'a': 'b'}), extractOptions("mail[a=b, ]"))
|
||||
|
||||
#self.assertRaises(ValueError, extractOptions ,'mail-how[')
|
||||
self.assertRaises(ValueError, extractOptions ,'mail-how[')
|
||||
|
||||
self.assertRaises(ValueError, extractOptions, """mail[a="test with interim (wrong) "" quotes"]""")
|
||||
self.assertRaises(ValueError, extractOptions, """mail[a='test with interim (wrong) '' quotes']""")
|
||||
self.assertRaises(ValueError, extractOptions, """mail[a='x, y, z', b=x, y, z]""")
|
||||
|
||||
self.assertRaises(ValueError, extractOptions, """mail['s']""")
|
||||
|
||||
# Empty option
|
||||
option = "abc[]"
|
||||
|
|
@ -455,8 +458,6 @@ class JailReaderTest(LogCaptureTestCase):
|
|||
('sender', 'f2b-test@example.com'), ('blocklist_de_apikey', 'test-key'),
|
||||
('action',
|
||||
'%(action_blocklist_de)s\n'
|
||||
'%(action_badips_report)s\n'
|
||||
'%(action_badips)s\n'
|
||||
'mynetwatchman[port=1234,protocol=udp,agent="%(fail2ban_agent)s"]'
|
||||
),
|
||||
))
|
||||
|
|
@ -470,16 +471,14 @@ class JailReaderTest(LogCaptureTestCase):
|
|||
if len(cmd) <= 4:
|
||||
continue
|
||||
# differentiate between set and multi-set (wrop it here to single set):
|
||||
if cmd[0] == 'set' and (cmd[4] == 'agent' or cmd[4].endswith('badips.py')):
|
||||
if cmd[0] == 'set' and cmd[4] == 'agent':
|
||||
act.append(cmd)
|
||||
elif cmd[0] == 'multi-set':
|
||||
act.extend([['set'] + cmd[1:4] + o for o in cmd[4] if o[0] == 'agent'])
|
||||
useragent = 'Fail2Ban/%s' % version
|
||||
self.assertEqual(len(act), 4)
|
||||
self.assertEqual(len(act), 2)
|
||||
self.assertEqual(act[0], ['set', 'blocklisttest', 'action', 'blocklist_de', 'agent', useragent])
|
||||
self.assertEqual(act[1], ['set', 'blocklisttest', 'action', 'badips', 'agent', useragent])
|
||||
self.assertEqual(eval(act[2][5]).get('agent', '<wrong>'), useragent)
|
||||
self.assertEqual(act[3], ['set', 'blocklisttest', 'action', 'mynetwatchman', 'agent', useragent])
|
||||
self.assertEqual(act[1], ['set', 'blocklisttest', 'action', 'mynetwatchman', 'agent', useragent])
|
||||
|
||||
@with_tmpdir
|
||||
def testGlob(self, d):
|
||||
|
|
@ -752,9 +751,9 @@ class JailsReaderTest(LogCaptureTestCase):
|
|||
['add', 'tz_correct', 'auto'],
|
||||
['start', 'tz_correct'],
|
||||
['config-error',
|
||||
"Jail 'brokenactiondef' skipped, because of wrong configuration: Invalid action definition 'joho[foo'"],
|
||||
"Jail 'brokenactiondef' skipped, because of wrong configuration: Invalid action definition 'joho[foo': unexpected option syntax"],
|
||||
['config-error',
|
||||
"Jail 'brokenfilterdef' skipped, because of wrong configuration: Invalid filter definition 'flt[test'"],
|
||||
"Jail 'brokenfilterdef' skipped, because of wrong configuration: Invalid filter definition 'flt[test': unexpected option syntax"],
|
||||
['config-error',
|
||||
"Jail 'missingaction' skipped, because of wrong configuration: Unable to read action 'noactionfileforthisaction'"],
|
||||
['config-error',
|
||||
|
|
@ -975,6 +974,7 @@ class JailsReaderTest(LogCaptureTestCase):
|
|||
['set', 'syslogsocket', 'auto'],
|
||||
['set', 'loglevel', "INFO"],
|
||||
['set', 'logtarget', '/var/log/fail2ban.log'],
|
||||
['set', 'allowipv6', 'auto'],
|
||||
['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'],
|
||||
['set', 'dbmaxmatches', 10],
|
||||
['set', 'dbpurgeage', '1d'],
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import tempfile
|
|||
import sqlite3
|
||||
import shutil
|
||||
|
||||
from ..server.filter import FileContainer
|
||||
from ..server.filter import FileContainer, Filter
|
||||
from ..server.mytime import MyTime
|
||||
from ..server.ticket import FailTicket
|
||||
from ..server.actions import Actions, Utils
|
||||
|
|
@ -212,19 +212,20 @@ class DatabaseTest(LogCaptureTestCase):
|
|||
self.jail.name in self.db.getJailNames(True),
|
||||
"Jail not added to database")
|
||||
|
||||
def testAddLog(self):
|
||||
def _testAddLog(self):
|
||||
self.testAddJail() # Jail required
|
||||
|
||||
_, filename = tempfile.mkstemp(".log", "Fail2BanDb_")
|
||||
self.fileContainer = FileContainer(filename, "utf-8")
|
||||
|
||||
self.db.addLog(self.jail, self.fileContainer)
|
||||
pos = self.db.addLog(self.jail, self.fileContainer)
|
||||
self.assertTrue(pos is None); # unknown previously
|
||||
|
||||
self.assertIn(filename, self.db.getLogPaths(self.jail))
|
||||
os.remove(filename)
|
||||
|
||||
def testUpdateLog(self):
|
||||
self.testAddLog() # Add log file
|
||||
self._testAddLog() # Add log file
|
||||
|
||||
# Write some text
|
||||
filename = self.fileContainer.getFileName()
|
||||
|
|
@ -544,17 +545,21 @@ class DatabaseTest(LogCaptureTestCase):
|
|||
self.testAddJail() # Jail required
|
||||
self.jail.database = self.db
|
||||
self.db.addJail(self.jail)
|
||||
actions = Actions(self.jail)
|
||||
actions = self.jail.actions
|
||||
actions.add(
|
||||
"action_checkainfo",
|
||||
os.path.join(TEST_FILES_DIR, "action.d/action_checkainfo.py"),
|
||||
{})
|
||||
actions.banManager.setBanTotal(20)
|
||||
self.jail._Jail__filter = flt = Filter(self.jail)
|
||||
flt.failManager.setFailTotal(50)
|
||||
ticket = FailTicket("1.2.3.4")
|
||||
ticket.setAttempt(5)
|
||||
ticket.setMatches(['test', 'test'])
|
||||
self.jail.putFailTicket(ticket)
|
||||
actions._Actions__checkBan()
|
||||
self.assertLogged("ban ainfo %s, %s, %s, %s" % (True, True, True, True))
|
||||
self.assertLogged("jail info %d, %d, %d, %d" % (1, 21, 0, 50))
|
||||
|
||||
def testDelAndAddJail(self):
|
||||
self.testAddJail() # Add jail
|
||||
|
|
|
|||
|
|
@ -554,6 +554,9 @@ class CustomDateFormatsTest(unittest.TestCase):
|
|||
(1123970401.0, "^%ExH:%ExM:%ExS**", '00:00:01'),
|
||||
# cover date with current year, in test cases now == Aug 2005 -> back to last year (Sep 2004):
|
||||
(1094068799.0, "^%m/%d %ExH:%ExM:%ExS**", '09/01 21:59:59'),
|
||||
# no time (only date) in pattern, assume local 00:00:00 for H:M:S :
|
||||
(1093989600.0, "^%Y-%m-%d**", '2004-09-01'),
|
||||
(1093996800.0, "^%Y-%m-%d%z**", '2004-09-01Z'),
|
||||
):
|
||||
logSys.debug('== test: %r', (matched, dp, line))
|
||||
dd = DateDetector()
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ def _start_params(tmp, use_stock=False, use_stock_cfg=None,
|
|||
os.symlink(os.path.abspath(pjoin(STOCK_CONF_DIR, n)), pjoin(cfg, n))
|
||||
if create_before_start:
|
||||
for n in create_before_start:
|
||||
_write_file(n % {'tmp': tmp}, 'w', '')
|
||||
_write_file(n % {'tmp': tmp}, 'w')
|
||||
# parameters (sock/pid and config, increase verbosity, set log, etc.):
|
||||
vvv, llev = (), "INFO"
|
||||
if unittest.F2B.log_level < logging.INFO: # pragma: no cover
|
||||
|
|
@ -937,10 +937,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
"Jail 'broken-jail' skipped, because of wrong configuration", all=True)
|
||||
|
||||
# enable both jails, 3 logs for jail1, etc...
|
||||
# truncate test-log - we should not find unban/ban again by reload:
|
||||
self.pruneLog("[test-phase 1b]")
|
||||
_write_jail_cfg(actions=[1,2])
|
||||
_write_file(test1log, "w+")
|
||||
if unittest.F2B.log_level < logging.DEBUG: # pragma: no cover
|
||||
_out_file(test1log)
|
||||
self.execCmd(SUCCESS, startparams, "reload")
|
||||
|
|
@ -1003,7 +1001,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
|
||||
self.pruneLog("[test-phase 2b]")
|
||||
# write new failures:
|
||||
_write_file(test2log, "w+", *(
|
||||
_write_file(test2log, "a+", *(
|
||||
(str(int(MyTime.time())) + " error 403 from 192.0.2.2: test 2",) * 3 +
|
||||
(str(int(MyTime.time())) + " error 403 from 192.0.2.3: test 2",) * 3 +
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.4: test 2",) * 3 +
|
||||
|
|
@ -1062,10 +1060,6 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
self.assertEqual(self.execCmdDirect(startparams,
|
||||
'get', 'test-jail1', 'banned', '192.0.2.3', '192.0.2.9')[1], [1, 0])
|
||||
|
||||
# rotate logs:
|
||||
_write_file(test1log, "w+")
|
||||
_write_file(test2log, "w+")
|
||||
|
||||
# restart jail without unban all:
|
||||
self.pruneLog("[test-phase 2c]")
|
||||
self.execCmd(SUCCESS, startparams,
|
||||
|
|
@ -1183,7 +1177,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
|
||||
# now write failures again and check already banned (jail1 was alive the whole time) and new bans occurred (jail1 was alive the whole time):
|
||||
self.pruneLog("[test-phase 5]")
|
||||
_write_file(test1log, "w+", *(
|
||||
_write_file(test1log, "a+", *(
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.1: test 5",) * 3 +
|
||||
(str(int(MyTime.time())) + " error 403 from 192.0.2.5: test 5",) * 3 +
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.6: test 5",) * 3
|
||||
|
|
@ -1326,7 +1320,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
'backend = polling',
|
||||
'usedns = no',
|
||||
'logpath = %(tmp)s/blck-failures.log',
|
||||
'action = nginx-block-map[blck_lst_reload="", blck_lst_file="%(tmp)s/blck-lst.map"]',
|
||||
'action = nginx-block-map[srv_cmd="echo nginx", srv_pid="%(tmp)s/f2b.pid", blck_lst_file="%(tmp)s/blck-lst.map"]',
|
||||
' blocklist_de[actionban=\'curl() { echo "*** curl" "$*";}; <Definition/actionban>\', email="Fail2Ban <fail2ban@localhost>", '
|
||||
'apikey="TEST-API-KEY", agent="fail2ban-test-agent", service=<name>]',
|
||||
'filter =',
|
||||
|
|
@ -1366,6 +1360,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
self.assertIn('\\125-000-004 1;\n', mp)
|
||||
self.assertIn('\\125-000-005 1;\n', mp)
|
||||
|
||||
# check nginx reload is logged (pid of fail2ban is used to simulate success check nginx is running):
|
||||
self.assertLogged("stdout: 'nginx -qt'", "stdout: 'nginx -s reload'", all=True)
|
||||
# check blocklist_de substitution (e. g. new-line after <matches>):
|
||||
self.assertLogged(
|
||||
"stdout: '*** curl --fail --data-urlencode server=Fail2Ban <fail2ban@localhost>"
|
||||
|
|
@ -1408,8 +1404,9 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
'jails': (
|
||||
# default:
|
||||
'''test_action = dummy[actionstart_on_demand=1, init="start: %(__name__)s", target="%(tmp)s/test.txt",
|
||||
actionban='<known/actionban>;
|
||||
echo "<matches>"; printf "=====\\n%%b\\n=====\\n\\n" "<matches>" >> <target>']''',
|
||||
actionban='<known/actionban>; echo "found: <jail.found> / <jail.found_total>, banned: <jail.banned> / <jail.banned_total>"
|
||||
echo "<matches>"; printf "=====\\n%%b\\n=====\\n\\n" "<matches>" >> <target>',
|
||||
actionstop='<known/actionstop>; echo "stats <name> - found: <jail.found_total>, banned: <jail.banned_total>"']''',
|
||||
# jail sendmail-auth:
|
||||
'[sendmail-auth]',
|
||||
'backend = polling',
|
||||
|
|
@ -1454,7 +1451,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
_write_file(lgfn, "w+", *smaut_msg)
|
||||
# wait and check it caused banned (and dump in the test-file):
|
||||
self.assertLogged(
|
||||
"[sendmail-auth] Ban 192.0.2.1", "1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
|
||||
"[sendmail-auth] Ban 192.0.2.1", "stdout: 'found: 0 / 3, banned: 1 / 1'",
|
||||
"1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
|
||||
_out_file(tofn)
|
||||
td = _read_file(tofn)
|
||||
# check matches (maxmatches = 2, so only 2 & 3 available):
|
||||
|
|
@ -1465,10 +1463,11 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
|
||||
self.pruneLog("[test-phase sendmail-reject]")
|
||||
# write log:
|
||||
_write_file(lgfn, "w+", *smrej_msg)
|
||||
_write_file(lgfn, "a+", *smrej_msg)
|
||||
# wait and check it caused banned (and dump in the test-file):
|
||||
self.assertLogged(
|
||||
"[sendmail-reject] Ban 192.0.2.2", "1 ticket(s) in 'sendmail-reject'", all=True, wait=MID_WAITTIME)
|
||||
"[sendmail-reject] Ban 192.0.2.2", "stdout: 'found: 0 / 3, banned: 1 / 1'",
|
||||
"1 ticket(s) in 'sendmail-reject'", all=True, wait=MID_WAITTIME)
|
||||
_out_file(tofn)
|
||||
td = _read_file(tofn)
|
||||
# check matches (no maxmatches, so all matched messages are available):
|
||||
|
|
@ -1482,6 +1481,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
# wait a bit:
|
||||
self.assertLogged(
|
||||
"Reload finished.",
|
||||
"stdout: 'stats sendmail-auth - found: 3, banned: 1'",
|
||||
"stdout: 'stats sendmail-reject - found: 3, banned: 1'",
|
||||
"[sendmail-auth] Restore Ban 192.0.2.1", "1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
|
||||
# check matches again - (dbmaxmatches = 1), so it should be only last match after restart:
|
||||
td = _read_file(tofn)
|
||||
|
|
@ -1590,7 +1591,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
wakeObs = False
|
||||
_observer_wait_before_incrban(lambda: wakeObs)
|
||||
# write again (IP already bad):
|
||||
_write_file(test1log, "w+", *(
|
||||
_write_file(test1log, "a+", *(
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm very bad \"hacker\" `` $(echo test)",) * 2
|
||||
))
|
||||
# wait for ban:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ __license__ = "GPL"
|
|||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from ..client import fail2banregex
|
||||
|
|
@ -80,6 +81,11 @@ def _test_exec_command_line(*args):
|
|||
sys.stderr = _org['stderr']
|
||||
return _exit_code
|
||||
|
||||
def _reset():
|
||||
# reset global warn-counter:
|
||||
from ..server.filter import _decode_line_warn
|
||||
_decode_line_warn.clear()
|
||||
|
||||
STR_00 = "Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0"
|
||||
STR_00_NODT = "[sshd] error: PAM: Authentication failure for kevin from 192.0.2.0"
|
||||
|
||||
|
|
@ -122,6 +128,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
|||
"""Call before every test case."""
|
||||
LogCaptureTestCase.setUp(self)
|
||||
setUpMyTime()
|
||||
_reset()
|
||||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
|
|
@ -141,6 +148,12 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
|||
))
|
||||
self.assertLogged("Unable to compile regular expression")
|
||||
|
||||
def testWrongFilterOptions(self):
|
||||
self.assertFalse(_test_exec(
|
||||
"test", "flt[a='x,y,z',b=z,y,x]"
|
||||
))
|
||||
self.assertLogged("Wrong filter name or options", "wrong syntax at 14: y,x", all=True)
|
||||
|
||||
def testDirectFound(self):
|
||||
self.assertTrue(_test_exec(
|
||||
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
|
||||
|
|
@ -395,7 +408,17 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
|||
"Found a match but no valid date/time found",
|
||||
"Match without a timestamp:", all=True)
|
||||
|
||||
self.pruneLog()
|
||||
def testIncompleteDateTime(self):
|
||||
# datepattern in followed lines doesn't match previously known pattern + line is too short
|
||||
# (logging break-off, no flush, etc):
|
||||
self.assertTrue(_test_exec(
|
||||
'-o', 'Found-ADDR:<ip>',
|
||||
'192.0.2.1 - - [02/May/2021:18:40:55 +0100] "GET / HTTP/1.1" 302 328 "-" "Mozilla/5.0" "-"\n'
|
||||
'192.0.2.2 - - [02/May/2021:18:40:55 +0100\n'
|
||||
'192.0.2.3 - - [02/May/2021:18:40:55',
|
||||
'^<ADDR>'))
|
||||
self.assertLogged(
|
||||
"Found-ADDR:192.0.2.1", "Found-ADDR:192.0.2.2", "Found-ADDR:192.0.2.3", all=True)
|
||||
|
||||
def testFrmtOutputWrapML(self):
|
||||
unittest.F2B.SkipIfCfgMissing(stock=True)
|
||||
|
|
@ -448,14 +471,8 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
|||
FILENAME_ZZZ_GEN, FILENAME_ZZZ_GEN
|
||||
))
|
||||
|
||||
def _reset(self):
|
||||
# reset global warn-counter:
|
||||
from ..server.filter import _decode_line_warn
|
||||
_decode_line_warn.clear()
|
||||
|
||||
def testWronChar(self):
|
||||
unittest.F2B.SkipIfCfgMissing(stock=True)
|
||||
self._reset()
|
||||
self.assertTrue(_test_exec(
|
||||
"-l", "notice", # put down log-level, because of too many debug-messages
|
||||
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
|
||||
|
|
@ -471,7 +488,6 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
|||
|
||||
def testWronCharDebuggex(self):
|
||||
unittest.F2B.SkipIfCfgMissing(stock=True)
|
||||
self._reset()
|
||||
self.assertTrue(_test_exec(
|
||||
"-l", "notice", # put down log-level, because of too many debug-messages
|
||||
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
|
||||
|
|
@ -484,6 +500,36 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
|||
|
||||
self.assertLogged('https://')
|
||||
|
||||
def testNLCharAsPartOfUniChar(self):
|
||||
fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='uni')
|
||||
# test two multi-byte encodings (both contains `\x0A` in either \x02\x0A or \x0A\x02):
|
||||
for enc in ('utf-16be', 'utf-16le'):
|
||||
self.pruneLog("[test-phase encoding=%s]" % enc)
|
||||
try:
|
||||
fout = open(fname, 'wb')
|
||||
# test on unicode string containing \x0A as part of uni-char,
|
||||
# it must produce exactly 2 lines (both are failures):
|
||||
for l in (
|
||||
u'1490349000 \u20AC Failed auth: invalid user Test\u020A from 192.0.2.1\n',
|
||||
u'1490349000 \u20AC Failed auth: invalid user TestI from 192.0.2.2\n'
|
||||
):
|
||||
fout.write(l.encode(enc))
|
||||
fout.close()
|
||||
|
||||
self.assertTrue(_test_exec(
|
||||
"-l", "notice", # put down log-level, because of too many debug-messages
|
||||
"--encoding", enc,
|
||||
"--datepattern", r"^EPOCH",
|
||||
fname, r"Failed .* from <HOST>",
|
||||
))
|
||||
|
||||
self.assertLogged(" encoding : %s" % enc,
|
||||
"Lines: 2 lines, 0 ignored, 2 matched, 0 missed", all=True)
|
||||
self.assertNotLogged("Missed line(s)")
|
||||
finally:
|
||||
fout.close()
|
||||
os.unlink(fname)
|
||||
|
||||
def testExecCmdLine_Usage(self):
|
||||
self.assertNotEqual(_test_exec_command_line(), 0)
|
||||
self.pruneLog()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ class TestAction(ActionBase):
|
|||
self._logSys.info("ban ainfo %s, %s, %s, %s",
|
||||
aInfo["ipmatches"] != '', aInfo["ipjailmatches"] != '', aInfo["ipfailures"] > 0, aInfo["ipjailfailures"] > 0
|
||||
)
|
||||
self._logSys.info("jail info %d, %d, %d, %d",
|
||||
aInfo["jail.banned"], aInfo["jail.banned_total"], aInfo["jail.found"], aInfo["jail.found_total"]
|
||||
)
|
||||
|
||||
def unban(self, aInfo):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
[Tue Mar 16 15:39:29 2010] [error] [client 58.179.109.179] Invalid URI in request \xf9h\xa9\xf3\x88\x8cXKj \xbf-l*4\x87n\xe4\xfe\xd4\x1d\x06\x8c\xf8m\\rS\xf6n\xeb\x8
|
||||
# failJSON: { "time": "2010-03-15T15:44:47", "match": true , "host": "121.222.2.133" }
|
||||
[Mon Mar 15 15:44:47 2010] [error] [client 121.222.2.133] Invalid URI in request n\xed*\xbe*\xab\xefd\x80\xb5\xae\xf6\x01\x10M?\xf2\xce\x13\x9c\xd7\xa0N\xa7\xdb%0\xde\xe0\xfc\xd2\xa0\xfe\xe9w\xee\xc4`v\x9b[{\x0c:\xcb\x93\xc6\xa0\x93\x9c`l\\\x8d\xc9
|
||||
# failJSON: { "time": "2010-03-15T16:04:06", "match": true , "host": "192.0.2.1", "desc": "AH00126 failure, gh-2908" }
|
||||
[Sat Mar 15 16:04:06.105212 2010] [core:error] [pid 17408] [client 192.0.2.1:55280] AH00126: Invalid URI in request GET /static/../../../a/../../../../etc/passwd HTTP/1.1
|
||||
|
||||
# http://forum.nconf.org/viewtopic.php?f=14&t=427&p=1488
|
||||
# failJSON: { "time": "2010-07-30T11:23:54", "match": true , "host": "10.85.6.69" }
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
[2012-02-13 17:44:26] NOTICE[1638] chan_iax2.c: Host 1.2.3.4 failed MD5 authentication for 'Fail2ban' (e7df7cd2ca07f4f1ab415d457a6e1c13 != 53ac4bc41ee4ec77888ed4aa50677247)
|
||||
# failJSON: { "time": "2013-02-05T23:44:42", "match": true , "host": "1.2.3.4" }
|
||||
[2013-02-05 23:44:42] NOTICE[436][C-00000fa9] chan_sip.c: Call from '' (1.2.3.4:10836) to extension '0972598285108' rejected because extension not found in context 'default'.
|
||||
# failJSON: { "time": "2005-01-18T17:39:50", "match": true , "host": "1.2.3.4" }
|
||||
[Jan 18 17:39:50] NOTICE[12049]: res_pjsip_session.c:2337 new_invite: Call from 'anonymous' (TCP:[1.2.3.4]:61470) to extension '9011+442037690237' rejected because extension not found in context 'default'.
|
||||
# failJSON: { "time": "2013-03-26T15:47:54", "match": true , "host": "1.2.3.4" }
|
||||
[2013-03-26 15:47:54] NOTICE[1237] chan_sip.c: Registration from '"100"sip:100@1.2.3.4' failed for '1.2.3.4:23930' - No matching peer found
|
||||
# failJSON: { "time": "2013-05-13T07:10:53", "match": true , "host": "1.2.3.4" }
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ Jul 02 13:49:32 hostname dovecot[442]: dovecot: auth(default): pam(account@MYSER
|
|||
# failJSON: { "time": "2005-01-29T05:32:50", "match": true , "host": "1.2.3.4" }
|
||||
Jan 29 05:32:50 mail dovecot: auth-worker(304): pam(username,1.2.3.4): pam_authenticate() failed: Authentication failure (password mismatch?)
|
||||
|
||||
# failJSON: { "time": "2005-01-29T18:55:55", "match": true , "host": "192.0.2.4", "desc": "Password mismatch (title case, gh-2880)" }
|
||||
Jan 29 18:55:55 mail dovecot: auth-worker(12182): pam(user,192.0.2.4): pam_authenticate() failed: Authentication failure (Password mismatch?)
|
||||
|
||||
# failJSON: { "time": "2005-01-29T05:13:40", "match": true , "host": "1.2.3.4" }
|
||||
Jan 29 05:13:40 mail dovecot: auth-worker(31326): pam(username,1.2.3.4): unknown user
|
||||
|
||||
|
|
@ -52,6 +55,11 @@ Jun 11 13:57:17 main dovecot: auth: sql(admin@example.ru,192.168.144.226,<6rXunF
|
|||
#failJSON: { "time": "2005-06-11T13:57:17", "match": true, "host": "192.168.178.25", "desc": "allow more verbose logging, gh-2573" }
|
||||
Jun 11 13:57:17 main dovecot: auth: ldap(user@server.org,192.168.178.25,<LZmGp6mZaMrAqLIZ>): Password mismatch (for LDAP bind) (SHA1 of given password: f638ff)
|
||||
|
||||
# failJSON: { "time": "2005-06-12T11:48:12", "match": true , "host": "192.0.2.6" }
|
||||
Jun 12 11:48:12 auth-worker(80180): Info: conn unix:auth-worker (uid=143): auth-worker<13247>: sql(support,192.0.2.6): unknown user
|
||||
# failJSON: { "time": "2005-06-12T23:06:05", "match": true , "host": "192.0.2.7" }
|
||||
Jun 12 23:06:05 auth-worker(57065): Info: conn unix:auth-worker (uid=143): auth-worker<225622>: sql(user@domain.com,192.0.2.7,<Yx7+W8+Io>): Password mismatch
|
||||
|
||||
# failJSON: { "time": "2005-01-29T14:38:51", "match": true , "host": "192.0.2.6", "desc": "PAM Permission denied (gh-1897)" }
|
||||
Jan 29 14:38:51 example.com dovecot[24941]: auth-worker(30165): pam(user@example.com,192.0.2.6,<PNHQq8pZhqIKAQGd>): pam_authenticate() failed: Permission denied
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,15 @@ Apr 26 13:15:25 webserver example.com: https://example.com|1430068525|user|1.2.3
|
|||
# failJSON: { "time": "2005-04-26T13:15:25", "match": true , "host": "1.2.3.4" }
|
||||
Apr 26 13:15:25 webserver example.com: https://example.com/subdir|1430068525|user|1.2.3.4|https://example.com/subdir/user|https://example.com/subdir/user|0||Login attempt failed for drupaladmin.
|
||||
|
||||
# failJSON: { "time": "2005-04-26T13:19:08", "match": false , "host": "1.2.3.4" }
|
||||
# failJSON: { "time": "2005-04-26T13:19:08", "match": false , "host": "1.2.3.4", "user": "drupaladmin" }
|
||||
Apr 26 13:19:08 webserver example.com: https://example.com|1430068748|user|1.2.3.4|https://example.com/user|https://example.com/user|1||Session opened for drupaladmin.
|
||||
|
||||
# failJSON: { "time": "2005-04-26T13:20:00", "match": false, "desc": "attempt to inject on URI (pipe, login failed for), not a failure, gh-2742" }
|
||||
Apr 26 13:20:00 host drupal-site: https://example.com|1613063581|user|192.0.2.5|https://example.com/user/login?test=%7C&test2=%7C...|https://example.com/user/login?test=|&test2=|0||Login attempt failed for tester|2||Session revisited for drupaladmin.
|
||||
|
||||
# failJSON: { "time": "2005-04-26T13:20:01", "match": true , "host": "192.0.2.7", "user": "Jack Sparrow", "desc": "log-format change - for -> from, user name with space, gh-2742" }
|
||||
Apr 26 13:20:01 mweb drupal_site[24864]: https://www.example.com|1613058599|user|192.0.2.7|https://www.example.com/en/user/login|https://www.example.com/en/user/login|0||Login attempt failed from Jack Sparrow.
|
||||
# failJSON: { "time": "2005-04-26T13:20:02", "match": true , "host": "192.0.2.4", "desc": "attempt to inject on URI (pipe), login failed, gh-2742" }
|
||||
Apr 26 13:20:02 host drupal-site: https://example.com|1613063581|user|192.0.2.4|https://example.com/user/login?test=%7C&test2=%7C|https://example.com/user/login?test=|&test2=||0||Login attempt failed from 192.0.2.4.
|
||||
# failJSON: { "time": "2005-04-26T13:20:03", "match": false, "desc": "attempt to inject on URI (pipe, login failed from), not a failure, gh-2742" }
|
||||
Apr 26 13:20:03 host drupal-site: https://example.com|1613063581|user|192.0.2.5|https://example.com/user/login?test=%7C&test2=%7C...|https://example.com/user/login?test=|&test2=|0||Login attempt failed from 1.2.3.4|2||Session revisited for drupaladmin.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@
|
|||
# failJSON: { "time": "2014-01-12T02:07:48", "match": true , "host": "85.214.85.40" }
|
||||
2014-01-12 02:07:48 dovecot_login authenticator failed for h1832461.stratoserver.net (User) [85.214.85.40]: 535 Incorrect authentication data (set_id=scanner)
|
||||
|
||||
# failJSON: { "time": "2019-10-22T03:39:17", "match": true , "host": "192.0.2.37", "desc": "pid-prefix in form of 'mx1 exim[...]:', gh-2553" }
|
||||
2019-10-22 03:39:17 mx1 exim[29786]: dovecot_login authenticator failed for (User) [192.0.2.37]: 535 Incorrect authentication data (set_id=test@domain.com)
|
||||
|
||||
# failJSON: { "time": "2014-12-02T03:00:23", "match": true , "host": "193.254.202.35" }
|
||||
2014-12-02 03:00:23 auth_plain authenticator failed for (rom182) [193.254.202.35]:41556 I=[10.0.0.1]:25: 535 Incorrect authentication data (set_id=webmaster)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
#authentification failure (mod_auth)
|
||||
# failJSON: { "time": "2011-12-25T17:09:20", "match": true , "host": "4.4.4.4" }
|
||||
2011-12-25 17:09:20: (http_auth.c.875) password doesn't match for /gitweb/ username: francois, IP: 4.4.4.4
|
||||
# failJSON: { "time": "2012-09-26T10:24:35", "match": true , "host": "4.4.4.4" }
|
||||
|
|
@ -7,3 +6,9 @@
|
|||
2013-08-25 00:24:55: (http_auth.c.877) get_password failed, IP: 4.4.4.4
|
||||
# failJSON: { "time": "2018-01-16T14:10:32", "match": true , "host": "192.0.2.1", "desc": "http_auth -> mod_auth, gh-2018" }
|
||||
2018-01-16 14:10:32: (mod_auth.c.525) password doesn't match for /test-url username: test, IP: 192.0.2.1
|
||||
# failJSON: { "time": "2021-09-30T16:05:33", "match": true , "host": "192.0.2.2", "user":"test", "desc": "gh-3116" }
|
||||
2021-09-30 16:05:33: mod_auth.c.828) password doesn't match for /secure/ username: test IP: 192.0.2.2
|
||||
# failJSON: { "time": "2021-09-30T17:44:37", "match": true , "host": "192.0.2.3", "user":"tester", "desc": "gh-3116" }
|
||||
2021-09-30 17:44:37: (mod_auth.c.791) digest: auth failed for tester : wrong password, IP: 192.0.2.3
|
||||
# failJSON: { "time": "2021-09-30T17:44:37", "match": true , "host": "192.0.2.4", "desc": "gh-3116" }
|
||||
2021-09-30 17:44:37: (mod_auth.c.791) digest: auth failed: uri mismatch (/uri1 != /uri2), IP: 192.0.2.4
|
||||
|
|
|
|||
8
fail2ban/tests/files/logs/monitorix
Normal file
8
fail2ban/tests/files/logs/monitorix
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# failJSON: { "time": "2021-04-14T08:11:01", "match": false, "desc": "should be ignored: successful request" }
|
||||
Wed Apr 14 08:11:01 2021 - OK - [127.0.0.1] "GET /monitorix-cgi/monitorix.cgi - Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0"
|
||||
# failJSON: { "time": "2021-04-14T08:54:22", "match": true, "host": "127.0.0.1", "desc": "file does not exist" }
|
||||
Wed Apr 14 08:54:22 2021 - NOTEXIST - [127.0.0.1] File does not exist: /manager/html
|
||||
# failJSON: { "time": "2021-04-14T11:24:31", "match": true, "host": "127.0.0.1", "desc": "access not allowed" }
|
||||
Wed Apr 14 11:24:31 2021 - NOTALLOWED - [127.0.0.1] Access not allowed: /monitorix/
|
||||
# failJSON: { "time": "2021-04-14T11:26:08", "match": true, "host": "127.0.0.1", "desc": "authentication error" }
|
||||
Wed Apr 14 11:26:08 2021 - AUTHERR - [127.0.0.1] Authentication error: /monitorix/
|
||||
11
fail2ban/tests/files/logs/mssql-auth
Normal file
11
fail2ban/tests/files/logs/mssql-auth
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# failJSON: { "time": "2020-02-24T16:05:21", "match": true , "host": "192.0.2.1" }
|
||||
2020-02-24 16:05:21.00 Logon Login failed for user 'Backend'. Reason: Could not find a login matching the name provided. [CLIENT: 192.0.2.1]
|
||||
# failJSON: { "time": "2020-02-24T16:30:25", "match": true , "host": "192.0.2.2" }
|
||||
2020-02-24 16:30:25.88 Logon Login failed for user '===)jf02hüas9ä##22f'. Reason: Could not find a login matching the name provided. [CLIENT: 192.0.2.2]
|
||||
# failJSON: { "time": "2020-02-24T16:31:12", "match": true , "host": "192.0.2.3" }
|
||||
2020-02-24 16:31:12.20 Logon Login failed for user ''. Reason: An attempt to login using SQL authentication failed. Server is configured for Integrated authentication only. [CLIENT: 192.0.2.3]
|
||||
|
||||
# failJSON: { "time": "2020-02-24T16:31:26", "match": true , "host": "192.0.2.4", "user":"O'Leary" }
|
||||
2020-02-24 16:31:26.01 Logon Login failed for user 'O'Leary'. Reason: Could not find a login matching the name provided. [CLIENT: 192.0.2.4]
|
||||
# failJSON: { "time": "2020-02-24T16:31:26", "match": false, "desc": "test injection in possibly unescaped foreign input" }
|
||||
2020-02-24 16:31:26.02 Wrong data received: Logon Login failed for user 'test'. Reason: Could not find a login matching the name provided. [CLIENT: 192.0.2.5]
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# filterOptions: [{"mode": "normal"}, {"mode": "auth"}]
|
||||
|
||||
# failJSON: { "time": "2012-04-09T11:53:29", "match": true , "host": "192.0.43.10" }
|
||||
2012/04/09 11:53:29 [error] 2865#0: *66647 user "xyz" was not found in "/var/www/.htpasswd", client: 192.0.43.10, server: www.myhost.com, request: "GET / HTTP/1.1", host: "www.myhost.com"
|
||||
|
|
@ -11,3 +12,20 @@
|
|||
2014/04/03 22:20:38 [error] 30708#0: *3 user "scriben dio": password mismatch, client: 192.0.2.1, server: , request: "GET / HTTP/1.1", host: "localhost:8443"
|
||||
# failJSON: { "time": "2014-04-03T22:20:40", "match": true, "host": "192.0.2.2", "desc": "trying injection on user name"}
|
||||
2014/04/03 22:20:40 [error] 30708#0: *3 user "test": password mismatch, client: 127.0.0.1, server: test, request: "GET / HTTP/1.1", host: "localhost:8443"": was not found in "/etc/nginx/.htpasswd", client: 192.0.2.2, server: , request: "GET / HTTP/1.1", host: "localhost:8443"
|
||||
|
||||
# filterOptions: [{"mode": "fallback"}]
|
||||
|
||||
# failJSON: { "time": "2020-11-25T14:42:16", "match": true , "host": "142.93.180.14" }
|
||||
2020/11/25 14:42:16 [crit] 76952#76952: *2454307 SSL_do_handshake() failed (SSL: error:1408F0C6:SSL routines:ssl3_get_record:packet length too long) while SSL handshaking, client: 142.93.180.14, server: 0.0.0.0:443
|
||||
# failJSON: { "time": "2020-11-25T15:47:47", "match": true , "host": "80.191.166.166" }
|
||||
2020/11/25 15:47:47 [crit] 76952#76952: *5062354 SSL_do_handshake() failed (SSL: error:1408F0A0:SSL routines:ssl3_get_record:length too short) while SSL handshaking, client: 80.191.166.166, server: 0.0.0.0:443
|
||||
# failJSON: { "time": "2020-11-25T16:48:08", "match": true , "host": "5.126.32.148" }
|
||||
2020/11/25 16:48:08 [crit] 76952#76952: *7976400 SSL_do_handshake() failed (SSL: error:1408F096:SSL routines:ssl3_get_record:encrypted length too long) while SSL handshaking, client: 5.126.32.148, server: 0.0.0.0:443
|
||||
# failJSON: { "time": "2020-11-25T16:02:45", "match": false }
|
||||
2020/11/25 16:02:45 [error] 76952#76952: *5645766 connect() failed (111: Connection refused) while connecting to upstream, client: 5.126.32.148, server: www.google.de, request: "GET /admin/config HTTP/2.0", upstream: "http://127.0.0.1:3000/admin/config", host: "www.google.de"
|
||||
|
||||
# filterOptions: [{"mode": "aggressive"}]
|
||||
# failJSON: { "time": "2020-11-25T14:42:16", "match": true , "host": "142.93.180.14" }
|
||||
2020/11/25 14:42:16 [crit] 76952#76952: *2454307 SSL_do_handshake() failed (SSL: error:1408F0C6:SSL routines:ssl3_get_record:packet length too long) while SSL handshaking, client: 142.93.180.14, server: 0.0.0.0:443
|
||||
# failJSON: { "time": "2012-04-09T11:53:29", "match": true , "host": "192.0.43.10" }
|
||||
2012/04/09 11:53:29 [error] 2865#0: *66647 user "xyz" was not found in "/var/www/.htpasswd", client: 192.0.43.10, server: www.myhost.com, request: "GET / HTTP/1.1", host: "www.myhost.com"
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@
|
|||
[1387288694] nsd[7745]: info: ratelimit block example.com. type any target 192.0.2.0/24 query 192.0.2.105 TYPE255
|
||||
# failJSON: { "time": "2013-12-18T07:42:15", "match": true , "host": "192.0.2.115" }
|
||||
[1387348935] nsd[23600]: info: axfr for zone domain.nl. from client 192.0.2.115 refused, no acl matches.
|
||||
# failJSON: { "time": "2021-03-05T05:25:14", "match": true , "host": "192.0.2.32", "desc": "new format, no client after from, no dot at end, gh-2965" }
|
||||
[2021-03-05 05:25:14.562] nsd[160800]: info: axfr for example.com. from 192.0.2.32 refused, no acl matches
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ Aug 10 10:55:38 f-vanier-bourgeois postfix/smtpd[2162]: NOQUEUE: reject: VRFY fr
|
|||
# failJSON: { "time": "2005-08-13T15:45:46", "match": true , "host": "192.0.2.1" }
|
||||
Aug 13 15:45:46 server postfix/smtpd[13844]: 00ADB3C0899: reject: RCPT from example.com[192.0.2.1]: 550 5.1.1 <sales@server.com>: Recipient address rejected: User unknown in local recipient table; from=<xxxxxx@example.com> to=<sales@server.com> proto=ESMTP helo=<mail.example.com>
|
||||
|
||||
# failJSON: { "time": "2005-05-19T00:00:30", "match": true , "host": "192.0.2.2", "desc": "undeliverable address (sender/recipient verification, gh-3039)" }
|
||||
May 19 00:00:30 proxy2 postfix/smtpd[16123]: NOQUEUE: reject: RCPT from example.net[192.0.2.2]: 550 5.1.1 <user1@example.com>: Recipient address rejected: undeliverable address: verification failed; from=<user2@example.org> to=<user1@example.com> proto=ESMTP helo=<example.net>
|
||||
|
||||
# failJSON: { "time": "2005-01-12T11:07:49", "match": true , "host": "181.21.131.88" }
|
||||
Jan 12 11:07:49 emf1pt2-2-35-70 postfix/smtpd[13767]: improper command pipelining after DATA from unknown[181.21.131.88]:
|
||||
|
||||
|
|
@ -35,6 +38,16 @@ Nov 22 22:33:44 xxx postfix/smtpd[11111]: NOQUEUE: reject: RCPT from 1-2-3-4.exa
|
|||
# failJSON: { "time": "2005-01-31T13:55:24", "match": true , "host": "78.107.251.238" }
|
||||
Jan 31 13:55:24 xxx postfix/smtpd[3462]: NOQUEUE: reject: EHLO from s271272.static.corbina.ru[78.107.251.238]: 504 5.5.2 <User>: Helo command rejected: need fully-qualified hostname; proto=SMTP helo=<User>
|
||||
|
||||
# failJSON: { "time": "2005-03-7T02:09:33", "match": true , "host": "192.0.2.151", "desc": "reject: DATA from, gh-2927" }
|
||||
Mar 7 02:09:33 server postfix/smtpd[27246]: 1D8CC1CA0A7F: milter-reject: DATA from 66-220-155-151.mail-mail.facebook.com[192.0.2.151]: 550 5.7.1 Command rejected; from=<security@mail.example.com> to=<hostmaster@example.com> proto=ESMTP helo=<192-0-2-151.mail-mail.example.com>
|
||||
# failJSON: { "time": "2005-03-11T23:27:54", "match": true , "host": "192.0.2.109", "desc": "reject: BDAT from, gh-2927" }
|
||||
Mar 11 23:27:54 server postfix-smo/submission/smtpd[22427]: 44JCRG5tYPzCqt2: reject: BDAT from signing-milter.example.com[192.0.2.109]: 550 5.5.3 <DATA>: Data command rejected: Multi-recipient bounce; from=<> to=<some@example.com> proto=ESMTP helo=<domain.tld>
|
||||
|
||||
# failJSON: { "time": "2005-04-06T13:05:01", "match": true , "host": "192.0.2.116", "desc": "RCPT from unknown, gh-2995" }
|
||||
Apr 6 13:05:01 server postfix/smtpd[20589]: NOQUEUE: reject: RCPT from unknown[192.0.2.116]: 504 5.5.2 <WIN-6A0KEE6QVP5>: Helo command rejected: need fully-qualified hostname; from=<spameri@example.com> to=<spameri@example.com> proto=ESMTP helo=<WIN-6A0KEE6QVP5>
|
||||
# failJSON: { "time": "2005-04-07T03:10:56", "match": true , "host": "192.0.2.246", "desc": "550 5.7.25 Client host rejected, gh-2996" }
|
||||
Apr 7 03:10:56 server postfix/smtpd[7754]: NOQUEUE: reject: RCPT from unknown[192.0.2.246]: 550 5.7.25 Client host rejected: cannot find your hostname, [192.0.2.246]; from=<laqqubtbyop@example.com> to=<sxhcpltqhpex@example.com> proto=ESMTP helo=<[192.0.2.246]>
|
||||
|
||||
# failJSON: { "time": "2005-01-31T13:55:24", "match": true , "host": "78.107.251.238" }
|
||||
Jan 31 13:55:24 xxx postfix-incoming/smtpd[3462]: NOQUEUE: reject: EHLO from s271272.static.corbina.ru[78.107.251.238]: 504 5.5.2 <User>: Helo command rejected: need fully-qualified hostname; proto=SMTP helo=<User>
|
||||
|
||||
|
|
@ -156,6 +169,12 @@ Dec 23 19:39:13 xxx postfix/postscreen[21057]: PREGREET 14 after 0.08 from [192.
|
|||
# failJSON: { "time": "2004-12-24T00:54:36", "match": true , "host": "192.0.2.3" }
|
||||
Dec 24 00:54:36 xxx postfix/postscreen[22515]: HANGUP after 16 from [192.0.2.3]:48119 in tests after SMTP handshake
|
||||
|
||||
# failJSON: { "time": "2005-06-08T23:14:28", "match": true , "host": "192.0.2.77", "desc": "abusive clients hitting command limit, see see http://www.postfix.org/POSTSCREEN_README.html (gh-3040)" }
|
||||
Jun 8 23:14:28 proxy2 postfix/postscreen[473]: COMMAND TIME LIMIT from [192.0.2.77]:3608 after CONNECT
|
||||
# failJSON: { "time": "2005-06-08T23:14:54", "match": true , "host": "192.0.2.26", "desc": "abusive clients hitting command limit (gh-3040)" }
|
||||
Jun 8 23:14:54 proxy2 postfix/postscreen[473]: COMMAND COUNT LIMIT from [192.0.2.26]:15592 after RCPT
|
||||
|
||||
|
||||
# filterOptions: [{}, {"mode": "ddos"}, {"mode": "aggressive"}]
|
||||
# failJSON: { "match": false, "desc": "don't affect lawful data (sporadical connection aborts within DATA-phase, see gh-1813 for discussion)" }
|
||||
Feb 18 09:50:05 xxx postfix/smtpd[42]: lost connection after DATA from good-host.example.com[192.0.2.10]
|
||||
|
|
|
|||
8
fail2ban/tests/files/logs/scanlogd
Normal file
8
fail2ban/tests/files/logs/scanlogd
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# failJSON: { "time": "2005-03-05T21:44:43", "match": true , "host": "192.0.2.123" }
|
||||
Mar 5 21:44:43 srv scanlogd: 192.0.2.123 to 192.0.2.1 ports 80, 81, 83, 88, 99, 443, 1080, 3128, ..., f????uxy, TOS 00, TTL 49 @20:44:43
|
||||
# failJSON: { "time": "2005-03-05T21:44:44", "match": true , "host": "192.0.2.123" }
|
||||
Mar 5 21:44:44 srv scanlogd: 192.0.2.123 to 192.0.2.1 ports 497, 515, 544, 543, 464, 513, ..., fSrpauxy, TOS 00 @09:04:25
|
||||
# failJSON: { "time": "2005-03-05T21:44:45", "match": true , "host": "192.0.2.123" }
|
||||
Mar 5 21:44:45 srv scanlogd: 192.0.2.123 to 192.0.2.1 ports 593, 548, 636, 646, 625, 631, ..., fSrpauxy, TOS 00, TTL 239 @17:34:00
|
||||
# failJSON: { "time": "2005-03-05T21:44:46", "match": true , "host": "192.0.2.123" }
|
||||
Mar 5 21:44:46 srv scanlogd: 192.0.2.123 to 192.0.2.1 ports 22, 26, 37, 80, 25, 79, ..., fSrpauxy, TOS 00 @22:38:37
|
||||
|
|
@ -22,3 +22,13 @@ Feb 24 14:00:01 server sendmail[3529566]: xA32R2PQ3529566: [192.0.2.2]: possible
|
|||
Feb 25 04:02:27 relay1 sendmail[16664]: 06I02CNi016764: AUTH failure (LOGIN): authentication failure (-13) SASL(-13): authentication failure: checkpass failed, user=user@example.com, relay=example.com [192.0.2.3] (may be forged)
|
||||
# failJSON: { "time": "2005-02-25T04:02:28", "match": true , "host": "192.0.2.4", "desc": "injection attempt on user name" }
|
||||
Feb 25 04:02:28 relay1 sendmail[16665]: 06I02CNi016765: AUTH failure (LOGIN): authentication failure (-13) SASL(-13): authentication failure: checkpass failed, user=criminal, relay=[192.0.2.100], relay=[192.0.2.4] (may be forged)
|
||||
|
||||
# failJSON: { "time": "2005-05-24T01:58:40", "match": true , "host": "192.0.2.5", "desc": "user not found (gh-3030)" }
|
||||
May 24 01:58:40 server sm-mta[65696]: 14NNwaRl065696: AUTH failure (DIGEST-MD5): user not found (-20) SASL(-13): user not found: unable to canonify user and get auxprops, user=scanner, relay=[192.0.2.5]
|
||||
# failJSON: { "time": "2005-05-24T01:59:07", "match": true , "host": "192.0.2.6", "desc": "user not found (gh-3030)" }
|
||||
May 24 01:59:07 server sm-mta[65815]: 14NNx65Q065815: AUTH failure (CRAM-MD5): user not found (-20) SASL(-13): user not found: user: scan@server.example.com property: userPassword not found in sasldb /usr/local/etc/sasldb2, user=scan, relay=[192.0.2.6]
|
||||
|
||||
# failJSON: { "time": "2005-05-29T23:14:04", "match": true , "host": "192.0.2.7", "desc": "authentication failure, sendmail 8.16.1 (gh-2757)" }
|
||||
May 29 23:14:04 mail sendmail[5976]: 09DJDgOM005976: AUTH failure (login): authentication failure (-13) SASL(-13): authentication failure: checkpass failed, user=test, relay=host.example.com [192.0.2.7] (may be forged)
|
||||
# failJSON: { "time": "2005-05-29T23:14:04", "match": true , "host": "192.0.2.8", "desc": "authentication failure, sendmail 8.16.1 (gh-2757)" }
|
||||
May 29 23:14:04 mail sendmail[5976]: 09DJDgOM005976: AUTH failure (PLAIN): authentication failure (-13) SASL(-13): authentication failure: Password verification failed, user=test, relay=host.example.com [192.0.2.8]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ Feb 27 15:49:07 batman sm-mta[88390]: ruleset=check_relay, arg1=189-30-205-74.pa
|
|||
# failJSON: { "time": "2005-02-19T18:01:50", "match": true , "host": "196.213.73.146" }
|
||||
Feb 19 18:01:50 batman sm-mta[78152]: ruleset=check_relay, arg1=[196.213.73.146], arg2=196.213.73.146, relay=[196.213.73.146], reject=421 4.3.2 Connection rate limit exceeded.
|
||||
|
||||
# failJSON: { "time": "2005-02-19T20:17:12", "match": true , "host": "192.0.2.123" }
|
||||
Feb 19 20:17:12 server sm-mta[201892]: ruleset=check_relay, arg1=[192.0.2.123], arg2=192.0.2.123, relay=host.example.com [192.0.2.123] (may be forged), reject=421 4.3.2 Connection rate limit exceeded.
|
||||
|
||||
# failJSON: { "time": "2005-02-27T10:53:06", "match": true , "host": "209.15.212.253" }
|
||||
Feb 27 10:53:06 batman sm-mta[44307]: s1R9r60D044307: rejecting commands from [209.15.212.253] due to pre-greeting traffic after 0 seconds
|
||||
# failJSON: { "time": "2005-02-27T10:53:07", "match": true , "host": "1.2.3.4" }
|
||||
|
|
@ -69,6 +72,8 @@ Feb 13 01:16:50 batman sm-mta[25815]: s1D0GoSs025815: [217.193.142.180]: vrfy in
|
|||
# failJSON: { "time": "2005-02-22T14:02:44", "match": true , "host": "24.73.201.194" }
|
||||
Feb 22 14:02:44 batman sm-mta[4030]: s1MD2hsd004030: rrcs-24-73-201-194.se.biz.rr.com [24.73.201.194]: VRFY root [rejected]
|
||||
|
||||
# failJSON: { "time": "2005-02-22T15:20:27", "match": true , "host": "192.0.2.5", "desc": "Fix reverse DNS for ... (gh-3012)" }
|
||||
Feb 22 15:20:27 localhost sm-mta[275631]: 13O9Ixhq275631: ruleset=check_rcpt, arg1=<linda@domain.com>, relay=[192.0.2.5], reject=550 5.7.1 <linda@domain.com>... Fix reverse DNS for 192.0.2.5
|
||||
|
||||
# failJSON: { "match": false }
|
||||
Nov 3 11:35:30 Microsoft sendmail[26254]: rA37ZTSC026250: <arhipov@domain.com>... No such user here
|
||||
|
|
@ -106,4 +111,4 @@ Mar 29 22:51:43 server sendmail[3529565]: xA32R2PQ3529565: [192.0.2.2] did not i
|
|||
# failJSON: { "time": "2005-03-29T22:51:45", "match": true , "host": "192.0.2.3", "desc": "sendmail 8.15.2 default names IPv4/6 (gh-2787)" }
|
||||
Mar 29 22:51:45 server sm-mta[50437]: 06QDQnNf050437: example.com [192.0.2.3] did not issue MAIL/EXPN/VRFY/ETRN during connection to IPv4
|
||||
# failJSON: { "time": "2005-03-29T22:51:46", "match": true , "host": "2001:DB8::1", "desc": "IPv6" }
|
||||
Mar 29 22:51:46 server sm-mta[50438]: 06QDQnNf050438: example.com [IPv6:2001:DB8::1] did not issue MAIL/EXPN/VRFY/ETRN during connection to IPv6
|
||||
Mar 29 22:51:46 server sm-mta[50438]: 06QDQnNf050438: example.com [IPv6:2001:DB8::1] did not issue MAIL/EXPN/VRFY/ETRN during connection to IPv6
|
||||
|
|
|
|||
|
|
@ -1,2 +1,8 @@
|
|||
# failJSON: { "time": "2016-03-28T16:50:49", "match": true , "host": "10.1.1.1" }
|
||||
[Mon Mar 28 16:50:49.522240 2016] [:error] [pid 1795] [client 10.1.1.1:50700] WAR [Login denied for user "username1"], referer: https://zoneminder/
|
||||
|
||||
# failJSON: { "time": "2021-03-28T16:53:00", "match": true , "host": "10.1.1.1" }
|
||||
[Sun Mar 28 16:53:00.472693 2021] [php7:notice] [pid 11328] [client 10.1.1.1:39568] ERR [Could not retrieve user username1 details], referer: https://zm/zm/?view=logout
|
||||
|
||||
# failJSON: { "time": "2021-03-28T16:59:14", "match": true , "host": "10.1.1.1" }
|
||||
[Sun Mar 28 16:59:14.150625 2021] [php7:notice] [pid 11336] [client 10.1.1.1:39654] ERR [Login denied for user "username1"], referer: https://zm/zm/?
|
||||
|
|
|
|||
|
|
@ -164,23 +164,31 @@ def _assert_correct_last_attempt(utest, filter_, output, count=None):
|
|||
# get fail ticket from jail
|
||||
found.append(_ticket_tuple(filter_.getFailTicket()))
|
||||
else:
|
||||
# when we are testing without jails
|
||||
# wait for failures (up to max time)
|
||||
Utils.wait_for(
|
||||
lambda: filter_.failManager.getFailCount() >= (tickcount, failcount),
|
||||
_maxWaitTime(10))
|
||||
# get fail ticket(s) from filter
|
||||
while tickcount:
|
||||
try:
|
||||
found.append(_ticket_tuple(filter_.failManager.toBan()))
|
||||
except FailManagerEmpty:
|
||||
break
|
||||
tickcount -= 1
|
||||
# when we are testing without jails wait for failures (up to max time)
|
||||
if filter_.jail:
|
||||
while True:
|
||||
t = filter_.jail.getFailTicket()
|
||||
if not t: break
|
||||
found.append(_ticket_tuple(t))
|
||||
if found:
|
||||
tickcount -= len(found)
|
||||
if tickcount > 0:
|
||||
Utils.wait_for(
|
||||
lambda: filter_.failManager.getFailCount() >= (tickcount, failcount),
|
||||
_maxWaitTime(10))
|
||||
# get fail ticket(s) from filter
|
||||
while tickcount:
|
||||
try:
|
||||
found.append(_ticket_tuple(filter_.failManager.toBan()))
|
||||
except FailManagerEmpty:
|
||||
break
|
||||
tickcount -= 1
|
||||
|
||||
if not isinstance(output[0], (tuple,list)):
|
||||
utest.assertEqual(len(found), 1)
|
||||
_assert_equal_entries(utest, found[0], output, count)
|
||||
else:
|
||||
utest.assertEqual(len(found), len(output))
|
||||
# sort by string representation of ip (multiple failures with different ips):
|
||||
found = sorted(found, key=lambda x: str(x))
|
||||
output = sorted(output, key=lambda x: str(x))
|
||||
|
|
@ -188,7 +196,7 @@ def _assert_correct_last_attempt(utest, filter_, output, count=None):
|
|||
_assert_equal_entries(utest, f, o)
|
||||
|
||||
|
||||
def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line=""):
|
||||
def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line="", lines=None):
|
||||
"""Copy lines from one file to another (which might be already open)
|
||||
|
||||
Returns open fout
|
||||
|
|
@ -205,9 +213,9 @@ def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line
|
|||
fin.readline()
|
||||
# Read
|
||||
i = 0
|
||||
lines = []
|
||||
if not lines: lines = []
|
||||
while n is None or i < n:
|
||||
l = FileContainer.decode_line(in_, 'UTF-8', fin.readline()).rstrip('\r\n')
|
||||
l = fin.readline().decode('UTF-8', 'replace').rstrip('\r\n')
|
||||
if terminal_line is not None and l == terminal_line:
|
||||
break
|
||||
lines.append(l)
|
||||
|
|
@ -215,6 +223,7 @@ def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line
|
|||
# Write: all at once and flush
|
||||
if isinstance(fout, str):
|
||||
fout = open(fout, mode)
|
||||
DefLogSys.debug(' ++ write %d test lines', len(lines))
|
||||
fout.write('\n'.join(lines)+'\n')
|
||||
fout.flush()
|
||||
if isinstance(in_, str): # pragma: no branch - only used with str in test cases
|
||||
|
|
@ -246,7 +255,7 @@ def _copy_lines_to_journal(in_, fields={},n=None, skip=0, terminal_line=""): # p
|
|||
# Read/Write
|
||||
i = 0
|
||||
while n is None or i < n:
|
||||
l = FileContainer.decode_line(in_, 'UTF-8', fin.readline()).rstrip('\r\n')
|
||||
l = fin.readline().decode('UTF-8', 'replace').rstrip('\r\n')
|
||||
if terminal_line is not None and l == terminal_line:
|
||||
break
|
||||
journal.send(MESSAGE=l.strip(), **fields)
|
||||
|
|
@ -436,11 +445,11 @@ class IgnoreIP(LogCaptureTestCase):
|
|||
def testTimeJump_InOperation(self):
|
||||
self._testTimeJump(inOperation=True)
|
||||
|
||||
def testWrongTimeZone(self):
|
||||
def testWrongTimeOrTZ(self):
|
||||
try:
|
||||
self.filter.addFailRegex('fail from <ADDR>$')
|
||||
self.filter.setDatePattern(r'{^LN-BEG}%Y-%m-%d %H:%M:%S(?:\s*%Z)?\s')
|
||||
self.filter.setMaxRetry(5); # don't ban here
|
||||
self.filter.setMaxRetry(50); # don't ban here
|
||||
self.filter.inOperation = True; # real processing (all messages are new)
|
||||
# current time is 1h later than log-entries:
|
||||
MyTime.setTime(1572138000+3600)
|
||||
|
|
@ -449,15 +458,18 @@ class IgnoreIP(LogCaptureTestCase):
|
|||
for i in (1,2,3):
|
||||
self.filter.processLineAndAdd('2019-10-27 02:00:00 fail from 192.0.2.15'); # +3 = 3
|
||||
self.assertLogged(
|
||||
"Simulate NOW in operation since found time has too large deviation",
|
||||
"Please check jail has possibly a timezone issue.",
|
||||
"Detected a log entry 1h before the current time in operation mode. This looks like a timezone problem.",
|
||||
"Please check a jail for a timing issue.",
|
||||
"192.0.2.15:1", "192.0.2.15:2", "192.0.2.15:3",
|
||||
"Total # of detected failures: 3.", wait=True)
|
||||
"Total # of detected failures: 3.", all=True, wait=True)
|
||||
#
|
||||
setattr(self.filter, "_next_simByTimeWarn", -1)
|
||||
self.pruneLog("[phase 2] wrong TZ given in log")
|
||||
for i in (1,2,3):
|
||||
self.filter.processLineAndAdd('2019-10-27 04:00:00 GMT fail from 192.0.2.16'); # +3 = 6
|
||||
self.assertLogged(
|
||||
"Detected a log entry 2h after the current time in operation mode. This looks like a timezone problem.",
|
||||
"Please check a jail for a timing issue.",
|
||||
"192.0.2.16:1", "192.0.2.16:2", "192.0.2.16:3",
|
||||
"Total # of detected failures: 6.", all=True, wait=True)
|
||||
self.assertNotLogged("Found a match but no valid date/time found")
|
||||
|
|
@ -470,6 +482,29 @@ class IgnoreIP(LogCaptureTestCase):
|
|||
"Match without a timestamp:",
|
||||
"192.0.2.17:1", "192.0.2.17:2", "192.0.2.17:3",
|
||||
"Total # of detected failures: 9.", all=True, wait=True)
|
||||
#
|
||||
phase = 3
|
||||
for delta, expect in (
|
||||
(-90*60, "timezone"), #90 minutes after
|
||||
(-60*60, "timezone"), #60 minutes after
|
||||
(-10*60, "timezone"), #10 minutes after
|
||||
(-59, None), #59 seconds after
|
||||
(59, None), #59 seconds before
|
||||
(61, "latency"), #>1 minute before
|
||||
(55*60, "latency"), #55 minutes before
|
||||
(90*60, "timezone") #90 minutes before
|
||||
):
|
||||
phase += 1
|
||||
MyTime.setTime(1572138000+delta)
|
||||
setattr(self.filter, "_next_simByTimeWarn", -1)
|
||||
self.pruneLog('[phase {phase}] log entries offset by {delta}s'.format(phase=phase, delta=delta))
|
||||
self.filter.processLineAndAdd('2019-10-27 02:00:00 fail from 192.0.2.15');
|
||||
self.assertLogged("Found 192.0.2.15", wait=True)
|
||||
if expect:
|
||||
self.assertLogged(("timezone problem", "latency problem")[int(expect == "latency")], all=True)
|
||||
self.assertNotLogged(("timezone problem", "latency problem")[int(expect != "latency")], all=True)
|
||||
else:
|
||||
self.assertNotLogged("timezone problem", "latency problem", all=True)
|
||||
finally:
|
||||
tearDownMyTime()
|
||||
|
||||
|
|
@ -599,13 +634,14 @@ class IgnoreIPDNS(LogCaptureTestCase):
|
|||
cmd = os.path.join(STOCK_CONF_DIR, "filter.d/ignorecommands/apache-fakegooglebot")
|
||||
## below test direct as python module:
|
||||
mod = Utils.load_python_module(cmd)
|
||||
self.assertFalse(mod.is_googlebot(mod.process_args([cmd, "128.178.222.69"])))
|
||||
self.assertFalse(mod.is_googlebot(mod.process_args([cmd, "192.0.2.1"])))
|
||||
self.assertFalse(mod.is_googlebot(*mod.process_args([cmd, "128.178.222.69"])))
|
||||
self.assertFalse(mod.is_googlebot(*mod.process_args([cmd, "192.0.2.1"])))
|
||||
self.assertFalse(mod.is_googlebot(*mod.process_args([cmd, "192.0.2.1", 0.1])))
|
||||
bot_ips = ['66.249.66.1']
|
||||
for ip in bot_ips:
|
||||
self.assertTrue(mod.is_googlebot(mod.process_args([cmd, str(ip)])), "test of googlebot ip %s failed" % ip)
|
||||
self.assertRaises(ValueError, lambda: mod.is_googlebot(mod.process_args([cmd])))
|
||||
self.assertRaises(ValueError, lambda: mod.is_googlebot(mod.process_args([cmd, "192.0"])))
|
||||
self.assertTrue(mod.is_googlebot(*mod.process_args([cmd, str(ip)])), "test of googlebot ip %s failed" % ip)
|
||||
self.assertRaises(ValueError, lambda: mod.is_googlebot(*mod.process_args([cmd])))
|
||||
self.assertRaises(ValueError, lambda: mod.is_googlebot(*mod.process_args([cmd, "192.0"])))
|
||||
## via command:
|
||||
self.filter.ignoreCommand = cmd + " <ip>"
|
||||
for ip in bot_ips:
|
||||
|
|
@ -617,7 +653,7 @@ class IgnoreIPDNS(LogCaptureTestCase):
|
|||
self.pruneLog()
|
||||
self.filter.ignoreCommand = cmd + " bad arguments <ip>"
|
||||
self.assertFalse(self.filter.inIgnoreIPList("192.0"))
|
||||
self.assertLogged('Please provide a single IP as an argument.')
|
||||
self.assertLogged('Usage')
|
||||
|
||||
|
||||
|
||||
|
|
@ -635,6 +671,19 @@ class LogFile(LogCaptureTestCase):
|
|||
self.filter = FilterPoll(None)
|
||||
self.assertRaises(IOError, self.filter.addLogPath, LogFile.MISSING)
|
||||
|
||||
def testDecodeLineWarn(self):
|
||||
# incomplete line (missing byte at end), warning is suppressed:
|
||||
l = u"correct line\n"
|
||||
r = l.encode('utf-16le')
|
||||
self.assertEqual(FileContainer.decode_line('TESTFILE', 'utf-16le', r), l)
|
||||
self.assertEqual(FileContainer.decode_line('TESTFILE', 'utf-16le', r[0:-1]), l[0:-1])
|
||||
self.assertNotLogged('Error decoding line')
|
||||
# complete line (incorrect surrogate in the middle), warning is there:
|
||||
r = b"incorrect \xc8\x0a line\n"
|
||||
l = r.decode('utf-8', 'replace')
|
||||
self.assertEqual(FileContainer.decode_line('TESTFILE', 'utf-8', r), l)
|
||||
self.assertLogged('Error decoding line')
|
||||
|
||||
|
||||
class LogFileFilterPoll(unittest.TestCase):
|
||||
|
||||
|
|
@ -800,7 +849,6 @@ class LogFileMonitor(LogCaptureTestCase):
|
|||
_, self.name = tempfile.mkstemp('fail2ban', 'monitorfailures')
|
||||
self.file = open(self.name, 'a')
|
||||
self.filter = FilterPoll(DummyJail())
|
||||
self.filter.banASAP = False # avoid immediate ban in this tests
|
||||
self.filter.addLogPath(self.name, autoSeek=False)
|
||||
self.filter.active = True
|
||||
self.filter.addFailRegex(r"(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>")
|
||||
|
|
@ -960,7 +1008,7 @@ class LogFileMonitor(LogCaptureTestCase):
|
|||
os.rename(self.name, self.name + '.bak')
|
||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=14, n=1).close()
|
||||
self.filter.getFailures(self.name)
|
||||
_assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01)
|
||||
#_assert_correct_last_attempt(self, self.filter, GetFailures.FAILURES_01)
|
||||
self.assertEqual(self.filter.failManager.getFailTotal(), 3)
|
||||
|
||||
|
||||
|
|
@ -971,6 +1019,10 @@ class CommonMonitorTestCase(unittest.TestCase):
|
|||
super(CommonMonitorTestCase, self).setUp()
|
||||
self._failTotal = 0
|
||||
|
||||
def tearDown(self):
|
||||
super(CommonMonitorTestCase, self).tearDown()
|
||||
self.assertFalse(hasattr(self, "_unexpectedError"))
|
||||
|
||||
def waitFailTotal(self, count, delay=1):
|
||||
"""Wait up to `delay` sec to assure that expected failure `count` reached
|
||||
"""
|
||||
|
|
@ -996,6 +1048,16 @@ class CommonMonitorTestCase(unittest.TestCase):
|
|||
last_ticks = self.filter.ticks
|
||||
return Utils.wait_for(lambda: self.filter.ticks >= last_ticks + ticks, _maxWaitTime(delay))
|
||||
|
||||
def commonFltError(self, reason="common", exc=None):
|
||||
""" Mock-up for default common error handler to find catched unhandled exceptions
|
||||
could occur in filters
|
||||
"""
|
||||
self._commonFltError(reason, exc)
|
||||
if reason == "unhandled":
|
||||
DefLogSys.critical("Caught unhandled exception in main cycle of %r : %r", self.filter, exc, exc_info=True)
|
||||
self._unexpectedError = True
|
||||
# self.assertNotEqual(reason, "unhandled")
|
||||
|
||||
|
||||
def get_monitor_failures_testcase(Filter_):
|
||||
"""Generator of TestCase's for different filters/backends
|
||||
|
|
@ -1018,7 +1080,8 @@ def get_monitor_failures_testcase(Filter_):
|
|||
self.file = open(self.name, 'a')
|
||||
self.jail = DummyJail()
|
||||
self.filter = Filter_(self.jail)
|
||||
self.filter.banASAP = False # avoid immediate ban in this tests
|
||||
# mock-up common error to find catched unhandled exceptions:
|
||||
self._commonFltError, self.filter.commonError = self.filter.commonError, self.commonFltError
|
||||
self.filter.addLogPath(self.name, autoSeek=False)
|
||||
# speedup search using exact date pattern:
|
||||
self.filter.setDatePattern(r'^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?')
|
||||
|
|
@ -1111,12 +1174,13 @@ def get_monitor_failures_testcase(Filter_):
|
|||
skip=12, n=3, mode='w')
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01)
|
||||
|
||||
def _wait4failures(self, count=2):
|
||||
def _wait4failures(self, count=2, waitEmpty=True):
|
||||
# Poll might need more time
|
||||
self.assertTrue(self.isEmpty(_maxWaitTime(5)),
|
||||
"Queue must be empty but it is not: %s."
|
||||
% (', '.join([str(x) for x in self.jail.queue])))
|
||||
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
|
||||
if waitEmpty:
|
||||
self.assertTrue(self.isEmpty(_maxWaitTime(5)),
|
||||
"Queue must be empty but it is not: %s."
|
||||
% (', '.join([str(x) for x in self.jail.queue])))
|
||||
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
|
||||
Utils.wait_for(lambda: self.filter.failManager.getFailTotal() >= count, _maxWaitTime(10))
|
||||
self.assertEqual(self.filter.failManager.getFailTotal(), count)
|
||||
|
||||
|
|
@ -1129,13 +1193,15 @@ def get_monitor_failures_testcase(Filter_):
|
|||
|
||||
# move aside, but leaving the handle still open...
|
||||
os.rename(self.name, self.name + '.bak')
|
||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=14, n=1).close()
|
||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=14, n=1,
|
||||
lines=["Aug 14 11:59:59 [logrotate] rotation 1"]).close()
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01)
|
||||
self.assertEqual(self.filter.failManager.getFailTotal(), 3)
|
||||
|
||||
# now remove the moved file
|
||||
_killfile(None, self.name + '.bak')
|
||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=12, n=3).close()
|
||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.name, skip=12, n=3,
|
||||
lines=["Aug 14 11:59:59 [logrotate] rotation 2"]).close()
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01)
|
||||
self.assertEqual(self.filter.failManager.getFailTotal(), 6)
|
||||
|
||||
|
|
@ -1189,7 +1255,7 @@ def get_monitor_failures_testcase(Filter_):
|
|||
os.rename(tmpsub1, tmpsub2 + 'a')
|
||||
os.mkdir(tmpsub1)
|
||||
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
|
||||
skip=12, n=1, mode='w')
|
||||
skip=12, n=1, mode='w', lines=["Aug 14 11:59:59 [logrotate] rotation 1"])
|
||||
self.file.close()
|
||||
self._wait4failures(2)
|
||||
|
||||
|
|
@ -1200,7 +1266,7 @@ def get_monitor_failures_testcase(Filter_):
|
|||
os.mkdir(tmpsub1)
|
||||
self.waitForTicks(2)
|
||||
self.file = _copy_lines_between_files(GetFailures.FILENAME_01, self.name,
|
||||
skip=12, n=1, mode='w')
|
||||
skip=12, n=1, mode='w', lines=["Aug 14 11:59:59 [logrotate] rotation 2"])
|
||||
self.file.close()
|
||||
self._wait4failures(3)
|
||||
|
||||
|
|
@ -1277,14 +1343,14 @@ def get_monitor_failures_testcase(Filter_):
|
|||
# tail written before, so let's not copy anything yet
|
||||
#_copy_lines_between_files(GetFailures.FILENAME_01, self.name, n=100)
|
||||
# we should detect the failures
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=6) # was needed if we write twice above
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=3) # was needed if we write twice above
|
||||
|
||||
# now copy and get even more
|
||||
_copy_lines_between_files(GetFailures.FILENAME_01, self.file, skip=12, n=3)
|
||||
# check for 3 failures (not 9), because 6 already get above...
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01)
|
||||
self.assert_correct_last_attempt(GetFailures.FAILURES_01, count=3)
|
||||
# total count in this test:
|
||||
self.assertEqual(self.filter.failManager.getFailTotal(), 12)
|
||||
self._wait4failures(12, False)
|
||||
|
||||
cls = MonitorFailures
|
||||
cls.__qualname__ = cls.__name__ = "MonitorFailures<%s>(%s)" \
|
||||
|
|
@ -1316,7 +1382,8 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
|
|||
def _initFilter(self, **kwargs):
|
||||
self._getRuntimeJournal() # check journal available
|
||||
self.filter = Filter_(self.jail, **kwargs)
|
||||
self.filter.banASAP = False # avoid immediate ban in this tests
|
||||
# mock-up common error to find catched unhandled exceptions:
|
||||
self._commonFltError, self.filter.commonError = self.filter.commonError, self.commonFltError
|
||||
self.filter.addJournalMatch([
|
||||
"SYSLOG_IDENTIFIER=fail2ban-testcases",
|
||||
"TEST_FIELD=1",
|
||||
|
|
@ -1345,7 +1412,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
|
|||
# check one at at time until the first hit
|
||||
for systemd_var in 'system-runtime-logs', 'system-state-logs':
|
||||
tmp = Utils.executeCmd(
|
||||
'find "$(systemd-path %s)" -name system.journal' % systemd_var,
|
||||
'find "$(systemd-path %s)/journal" -name system.journal -readable' % systemd_var,
|
||||
timeout=10, shell=True, output=True
|
||||
)
|
||||
self.assertTrue(tmp)
|
||||
|
|
@ -1407,7 +1474,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
|
|||
if idle:
|
||||
self.filter.sleeptime /= 100.0
|
||||
self.filter.idle = True
|
||||
self.waitForTicks(1)
|
||||
self.waitForTicks(1)
|
||||
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
|
||||
|
||||
# Now let's feed it with entries from the file
|
||||
|
|
@ -1486,9 +1553,33 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
|
|||
_gen_falure("192.0.2.6")
|
||||
self.assertFalse(self.jail.getFailTicket())
|
||||
|
||||
# now reset DB, so we'd find all messages before filter entering in operation mode:
|
||||
self.filter.stop()
|
||||
self.filter.join()
|
||||
self.jail.database.updateJournal(self.jail, 'systemd-journal', MyTime.time()-10000, 'TEST')
|
||||
self._initFilter()
|
||||
self.filter.setMaxRetry(1)
|
||||
states = []
|
||||
def _state(*args):
|
||||
self.assertNotIn("** in operation", states)
|
||||
self.assertFalse(self.filter.inOperation)
|
||||
states.append("** process line: %r" % (args,))
|
||||
self.filter.processLineAndAdd = _state
|
||||
def _inoper():
|
||||
self.assertNotIn("** in operation", states)
|
||||
self.assertEqual(len(states), 11)
|
||||
states.append("** in operation")
|
||||
self.filter.__class__.inOperationMode(self.filter)
|
||||
self.filter.inOperationMode = _inoper
|
||||
self.filter.start()
|
||||
self.waitForTicks(12)
|
||||
self.assertTrue(Utils.wait_for(lambda: len(states) == 12, _maxWaitTime(10)))
|
||||
self.assertEqual(states[-1], "** in operation")
|
||||
|
||||
def test_delJournalMatch(self):
|
||||
self._initFilter()
|
||||
self.filter.start()
|
||||
self.waitForTicks(1); # wait for start
|
||||
# Smoke test for removing of match
|
||||
|
||||
# basic full test
|
||||
|
|
@ -1512,7 +1603,7 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
|
|||
"SYSLOG_IDENTIFIER=fail2ban-testcases",
|
||||
"TEST_FIELD=1",
|
||||
"TEST_UUID=%s" % self.test_uuid])
|
||||
self.assert_correct_ban("193.168.0.128", 4)
|
||||
self.assert_correct_ban("193.168.0.128", 3)
|
||||
_copy_lines_to_journal(
|
||||
self.test_file, self.journal_fields, n=6, skip=10)
|
||||
# we should detect the failures
|
||||
|
|
@ -1521,12 +1612,13 @@ def get_monitor_failures_journal_testcase(Filter_): # pragma: systemd no cover
|
|||
def test_WrongChar(self):
|
||||
self._initFilter()
|
||||
self.filter.start()
|
||||
self.waitForTicks(1); # wait for start
|
||||
# Now let's feed it with entries from the file
|
||||
_copy_lines_to_journal(
|
||||
self.test_file, self.journal_fields, skip=15, n=4)
|
||||
self.waitForTicks(1)
|
||||
self.assertTrue(self.isFilled(10))
|
||||
self.assert_correct_ban("87.142.124.10", 4)
|
||||
self.assert_correct_ban("87.142.124.10", 3)
|
||||
# Add direct utf, unicode, blob:
|
||||
for l in (
|
||||
"error: PAM: Authentication failure for \xe4\xf6\xfc\xdf from 192.0.2.1",
|
||||
|
|
@ -1570,7 +1662,6 @@ class GetFailures(LogCaptureTestCase):
|
|||
setUpMyTime()
|
||||
self.jail = DummyJail()
|
||||
self.filter = FileFilter(self.jail)
|
||||
self.filter.banASAP = False # avoid immediate ban in this tests
|
||||
self.filter.active = True
|
||||
# speedup search using exact date pattern:
|
||||
self.filter.setDatePattern(r'^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?')
|
||||
|
|
@ -1625,22 +1716,56 @@ class GetFailures(LogCaptureTestCase):
|
|||
def testCRLFFailures01(self):
|
||||
# We first adjust logfile/failures to end with CR+LF
|
||||
fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='crlf')
|
||||
# poor man unix2dos:
|
||||
fin, fout = open(GetFailures.FILENAME_01, 'rb'), open(fname, 'wb')
|
||||
for l in fin.read().splitlines():
|
||||
fout.write(l + b'\r\n')
|
||||
fin.close()
|
||||
fout.close()
|
||||
try:
|
||||
# poor man unix2dos:
|
||||
fin, fout = open(GetFailures.FILENAME_01, 'rb'), open(fname, 'wb')
|
||||
for l in fin.read().splitlines():
|
||||
fout.write(l + b'\r\n')
|
||||
fin.close()
|
||||
fout.close()
|
||||
|
||||
# now see if we should be getting the "same" failures
|
||||
self.testGetFailures01(filename=fname)
|
||||
_killfile(fout, fname)
|
||||
# now see if we should be getting the "same" failures
|
||||
self.testGetFailures01(filename=fname)
|
||||
finally:
|
||||
_killfile(fout, fname)
|
||||
|
||||
def testNLCharAsPartOfUniChar(self):
|
||||
fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='uni')
|
||||
# test two multi-byte encodings (both contains `\x0A` in either \x02\x0A or \x0A\x02):
|
||||
for enc in ('utf-16be', 'utf-16le'):
|
||||
self.pruneLog("[test-phase encoding=%s]" % enc)
|
||||
try:
|
||||
fout = open(fname, 'wb')
|
||||
tm = int(time.time())
|
||||
# test on unicode string containing \x0A as part of uni-char,
|
||||
# it must produce exactly 2 lines (both are failures):
|
||||
for l in (
|
||||
u'%s \u20AC Failed auth: invalid user Test\u020A from 192.0.2.1\n' % tm,
|
||||
u'%s \u20AC Failed auth: invalid user TestI from 192.0.2.2\n' % tm
|
||||
):
|
||||
fout.write(l.encode(enc))
|
||||
fout.close()
|
||||
|
||||
self.filter.setLogEncoding(enc)
|
||||
self.filter.addLogPath(fname, autoSeek=0)
|
||||
self.filter.setDatePattern((r'^EPOCH',))
|
||||
self.filter.addFailRegex(r"Failed .* from <HOST>")
|
||||
self.filter.getFailures(fname)
|
||||
self.assertLogged(
|
||||
"[DummyJail] Found 192.0.2.1",
|
||||
"[DummyJail] Found 192.0.2.2", all=True, wait=True)
|
||||
finally:
|
||||
_killfile(fout, fname)
|
||||
self.filter.delLogPath(fname)
|
||||
# must find 4 failures and generate 2 tickets (2 IPs with each 2 failures):
|
||||
self.assertEqual(self.filter.failManager.getFailCount(), (2, 4))
|
||||
|
||||
def testGetFailures02(self):
|
||||
output = ('141.3.81.106', 4, 1124013539.0,
|
||||
[u'Aug 14 11:%d:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:141.3.81.106 port 51332 ssh2'
|
||||
% m for m in 53, 54, 57, 58])
|
||||
|
||||
self.filter.setMaxRetry(4)
|
||||
self.filter.addLogPath(GetFailures.FILENAME_02, autoSeek=0)
|
||||
self.filter.addFailRegex(r"Failed .* from <HOST>")
|
||||
self.filter.getFailures(GetFailures.FILENAME_02)
|
||||
|
|
@ -1649,6 +1774,7 @@ class GetFailures(LogCaptureTestCase):
|
|||
def testGetFailures03(self):
|
||||
output = ('203.162.223.135', 6, 1124013600.0)
|
||||
|
||||
self.filter.setMaxRetry(6)
|
||||
self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=0)
|
||||
self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown")
|
||||
self.filter.getFailures(GetFailures.FILENAME_03)
|
||||
|
|
@ -1657,6 +1783,7 @@ class GetFailures(LogCaptureTestCase):
|
|||
def testGetFailures03_InOperation(self):
|
||||
output = ('203.162.223.135', 9, 1124013600.0)
|
||||
|
||||
self.filter.setMaxRetry(9)
|
||||
self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=0)
|
||||
self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown")
|
||||
self.filter.getFailures(GetFailures.FILENAME_03, inOperation=True)
|
||||
|
|
@ -1674,7 +1801,7 @@ class GetFailures(LogCaptureTestCase):
|
|||
def testGetFailures03_Seek2(self):
|
||||
# same test as above but with seek to 'Aug 14 11:59:04' - so other output ...
|
||||
output = ('203.162.223.135', 2, 1124013600.0)
|
||||
self.filter.setMaxRetry(1)
|
||||
self.filter.setMaxRetry(2)
|
||||
|
||||
self.filter.addLogPath(GetFailures.FILENAME_03, autoSeek=output[2])
|
||||
self.filter.addFailRegex(r"error,relay=<HOST>,.*550 User unknown")
|
||||
|
|
@ -1684,10 +1811,12 @@ class GetFailures(LogCaptureTestCase):
|
|||
def testGetFailures04(self):
|
||||
# because of not exact time in testcase04.log (no year), we should always use our test time:
|
||||
self.assertEqual(MyTime.time(), 1124013600)
|
||||
# should find exact 4 failures for *.186 and 2 failures for *.185
|
||||
output = (('212.41.96.186', 4, 1124013600.0),
|
||||
('212.41.96.185', 2, 1124013598.0))
|
||||
|
||||
# should find exact 4 failures for *.186 and 2 failures for *.185, but maxretry is 2, so 3 tickets:
|
||||
output = (
|
||||
('212.41.96.186', 2, 1124013480.0),
|
||||
('212.41.96.186', 2, 1124013600.0),
|
||||
('212.41.96.185', 2, 1124013598.0)
|
||||
)
|
||||
# speedup search using exact date pattern:
|
||||
self.filter.setDatePattern((r'^%ExY(?P<_sep>[-/.])%m(?P=_sep)%d[T ]%H:%M:%S(?:[.,]%f)?(?:\s*%z)?',
|
||||
r'^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?',
|
||||
|
|
@ -1744,9 +1873,11 @@ class GetFailures(LogCaptureTestCase):
|
|||
unittest.F2B.SkipIfNoNetwork()
|
||||
# We should still catch failures with usedns = no ;-)
|
||||
output_yes = (
|
||||
('93.184.216.34', 2, 1124013539.0,
|
||||
[u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2',
|
||||
u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2']
|
||||
('93.184.216.34', 1, 1124013299.0,
|
||||
[u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2']
|
||||
),
|
||||
('93.184.216.34', 1, 1124013539.0,
|
||||
[u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.34 port 51332 ssh2']
|
||||
),
|
||||
('2606:2800:220:1:248:1893:25c8:1946', 1, 1124013299.0,
|
||||
[u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2']
|
||||
|
|
@ -1771,7 +1902,6 @@ class GetFailures(LogCaptureTestCase):
|
|||
self.pruneLog("[test-phase useDns=%s]" % useDns)
|
||||
jail = DummyJail()
|
||||
filter_ = FileFilter(jail, useDns=useDns)
|
||||
filter_.banASAP = False # avoid immediate ban in this tests
|
||||
filter_.active = True
|
||||
filter_.failManager.setMaxRetry(1) # we might have just few failures
|
||||
|
||||
|
|
@ -1781,8 +1911,11 @@ class GetFailures(LogCaptureTestCase):
|
|||
_assert_correct_last_attempt(self, filter_, output)
|
||||
|
||||
def testGetFailuresMultiRegex(self):
|
||||
output = ('141.3.81.106', 8, 1124013541.0)
|
||||
output = [
|
||||
('141.3.81.106', 8, 1124013541.0)
|
||||
]
|
||||
|
||||
self.filter.setMaxRetry(8)
|
||||
self.filter.addLogPath(GetFailures.FILENAME_02, autoSeek=False)
|
||||
self.filter.addFailRegex(r"Failed .* from <HOST>")
|
||||
self.filter.addFailRegex(r"Accepted .* from <HOST>")
|
||||
|
|
@ -1800,26 +1933,25 @@ class GetFailures(LogCaptureTestCase):
|
|||
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
|
||||
|
||||
def testGetFailuresMultiLine(self):
|
||||
output = [("192.0.43.10", 2, 1124013599.0),
|
||||
("192.0.43.11", 1, 1124013598.0)]
|
||||
output = [
|
||||
("192.0.43.10", 1, 1124013598.0),
|
||||
("192.0.43.10", 1, 1124013599.0),
|
||||
("192.0.43.11", 1, 1124013598.0)
|
||||
]
|
||||
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE, autoSeek=False)
|
||||
self.filter.setMaxLines(100)
|
||||
self.filter.addFailRegex(r"^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
|
||||
self.filter.setMaxRetry(1)
|
||||
|
||||
self.filter.getFailures(GetFailures.FILENAME_MULTILINE)
|
||||
|
||||
foundList = []
|
||||
while True:
|
||||
try:
|
||||
foundList.append(
|
||||
_ticket_tuple(self.filter.failManager.toBan())[0:3])
|
||||
except FailManagerEmpty:
|
||||
break
|
||||
self.assertSortedEqual(foundList, output)
|
||||
|
||||
_assert_correct_last_attempt(self, self.filter, output)
|
||||
|
||||
def testGetFailuresMultiLineIgnoreRegex(self):
|
||||
output = [("192.0.43.10", 2, 1124013599.0)]
|
||||
output = [
|
||||
("192.0.43.10", 1, 1124013598.0),
|
||||
("192.0.43.10", 1, 1124013599.0)
|
||||
]
|
||||
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE, autoSeek=False)
|
||||
self.filter.setMaxLines(100)
|
||||
self.filter.addFailRegex(r"^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
|
||||
|
|
@ -1828,14 +1960,17 @@ class GetFailures(LogCaptureTestCase):
|
|||
|
||||
self.filter.getFailures(GetFailures.FILENAME_MULTILINE)
|
||||
|
||||
_assert_correct_last_attempt(self, self.filter, output.pop())
|
||||
_assert_correct_last_attempt(self, self.filter, output)
|
||||
|
||||
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
|
||||
|
||||
def testGetFailuresMultiLineMultiRegex(self):
|
||||
output = [("192.0.43.10", 2, 1124013599.0),
|
||||
output = [
|
||||
("192.0.43.10", 1, 1124013598.0),
|
||||
("192.0.43.10", 1, 1124013599.0),
|
||||
("192.0.43.11", 1, 1124013598.0),
|
||||
("192.0.43.15", 1, 1124013598.0)]
|
||||
("192.0.43.15", 1, 1124013598.0)
|
||||
]
|
||||
self.filter.addLogPath(GetFailures.FILENAME_MULTILINE, autoSeek=False)
|
||||
self.filter.setMaxLines(100)
|
||||
self.filter.addFailRegex(r"^.*rsyncd\[(?P<pid>\d+)\]: connect from .+ \(<HOST>\)$<SKIPLINES>^.+ rsyncd\[(?P=pid)\]: rsync error: .*$")
|
||||
|
|
@ -1844,14 +1979,9 @@ class GetFailures(LogCaptureTestCase):
|
|||
|
||||
self.filter.getFailures(GetFailures.FILENAME_MULTILINE)
|
||||
|
||||
foundList = []
|
||||
while True:
|
||||
try:
|
||||
foundList.append(
|
||||
_ticket_tuple(self.filter.failManager.toBan())[0:3])
|
||||
except FailManagerEmpty:
|
||||
break
|
||||
self.assertSortedEqual(foundList, output)
|
||||
_assert_correct_last_attempt(self, self.filter, output)
|
||||
|
||||
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
|
||||
|
||||
|
||||
class DNSUtilsTests(unittest.TestCase):
|
||||
|
|
@ -2192,6 +2322,7 @@ class DNSUtilsNetworkTests(unittest.TestCase):
|
|||
ip1 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); ip2 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); self.assertEqual(id(ip1), id(ip2))
|
||||
|
||||
def testFQDN(self):
|
||||
unittest.F2B.SkipIfNoNetwork()
|
||||
sname = DNSUtils.getHostname(fqdn=False)
|
||||
lname = DNSUtils.getHostname(fqdn=True)
|
||||
# FQDN is not localhost if short hostname is not localhost too (or vice versa):
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class SetupTest(unittest.TestCase):
|
|||
supdbgout = ' >/dev/null 2>&1' if unittest.F2B.log_level >= logging.DEBUG else '' # HEAVYDEBUG
|
||||
try:
|
||||
# try dry-run:
|
||||
os.system("%s %s --dry-run install --disable-2to3 --root=%s%s"
|
||||
os.system("%s %s --dry-run install --root=%s%s"
|
||||
% (sys.executable, self.setup , tmp, supdbgout))
|
||||
# check nothing was created:
|
||||
self.assertTrue(not os.listdir(tmp))
|
||||
|
|
@ -127,7 +127,7 @@ class SetupTest(unittest.TestCase):
|
|||
# suppress stdout (and stderr) if not heavydebug
|
||||
supdbgout = ' >/dev/null' if unittest.F2B.log_level >= logging.DEBUG else '' # HEAVYDEBUG
|
||||
try:
|
||||
self.assertEqual(os.system("%s %s install --disable-2to3 --root=%s%s"
|
||||
self.assertEqual(os.system("%s %s install --root=%s%s"
|
||||
% (sys.executable, self.setup, tmp, supdbgout)), 0)
|
||||
|
||||
def strippath(l):
|
||||
|
|
@ -457,3 +457,18 @@ class MyTimeTest(unittest.TestCase):
|
|||
self.assertEqual(float(str2sec("1 month")) / 60 / 60 / 24, 30.4375)
|
||||
self.assertEqual(float(str2sec("1 year")) / 60 / 60 / 24, 365.25)
|
||||
|
||||
def testSec2Str(self):
|
||||
sec2str = lambda s: str(MyTime.seconds2str(s))
|
||||
self.assertEqual(sec2str(86400*390), '1y 3w 4d')
|
||||
self.assertEqual(sec2str(86400*368), '1y 3d')
|
||||
self.assertEqual(sec2str(86400*365.49), '1y')
|
||||
self.assertEqual(sec2str(86400*15), '2w 1d')
|
||||
self.assertEqual(sec2str(86400*14-10), '2w')
|
||||
self.assertEqual(sec2str(86400*2+3600*7+60*15), '2d 7h 15m')
|
||||
self.assertEqual(sec2str(86400*2+3599), '2d 1h')
|
||||
self.assertEqual(sec2str(3600*3.52), '3h 31m')
|
||||
self.assertEqual(sec2str(3600*2-5), '2h')
|
||||
self.assertEqual(sec2str(3600-5), '1h')
|
||||
self.assertEqual(sec2str(3600-10), '59m 50s')
|
||||
self.assertEqual(sec2str(59), '59s')
|
||||
self.assertEqual(sec2str(0), '0')
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue