diff --git a/doc/builtinfilters.md b/doc/builtinfilters.md index 1b6f699..020d2f0 100644 --- a/doc/builtinfilters.md +++ b/doc/builtinfilters.md @@ -4,7 +4,7 @@ Pyruse comes with a few very simple filters. ## `=`, `≤`, `≥`, `in` -The filters `filter_equals`, `filter_lowerOrEquals`, and `filter_greaterOrEquals` simply check equality on inequality between a given field, given by the parameter `field`, and a constant value, given py the parameter `value`. +The filters `filter_equals`, `filter_lowerOrEquals`, and `filter_greaterOrEquals` simply check equality or inequality between a given field, given by the parameter `field`, and a constant value, given py the parameter `value`. Both parameters are mandatory. Here are two examples: @@ -32,6 +32,21 @@ Here is an example: For any of these filters, the constant values must be of the same type as the typical contents of the chosen field. +## Test if an IP address is part of given networks + +Filter `filter_inNetworks` reads an IP address in a field given by the `field` parameter, and a list of networks in the `nets` parameter; each net is written as an IP address, then “`/`”, then an integer network mask. + +The filter is passing if the IP address that was read is part of one of the networks configured for the filter. + +Here is an example: + +```json +{ + "filter": "filter_inNetworks", + "args": { "field": "IP", "nets": [ "fd00::/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" ] } +} +``` + ## Perl-compatible regular expressions (pcre) Filter `filter_pcre` should only be used on character strings. diff --git a/extra/examples/full_pyruse.json b/extra/examples/full_pyruse.json index 1baf49f..a292f03 100644 --- a/extra/examples/full_pyruse.json +++ b/extra/examples/full_pyruse.json @@ -86,14 +86,22 @@ { "action": "action_dailyReport", "args": { "level": "WARN", "message": "Failed login as {thatUser}@{_HOSTNAME} on Nextcloud" }, - "then": "… Detect repeated Nextcloud login failures" + "then": "… Do not ban local Nextcloud users" } ], "… Report inexisting Nextcloud user": [ { "action": "action_dailyReport", "args": { "level": "INFO", "message": "Failed login as {thatUser}@{_HOSTNAME} on Nextcloud" }, - "then": "… Detect repeated Nextcloud login failures" + "then": "… Do not ban local Nextcloud users" + } + ], + "… Do not ban local Nextcloud users": [ + { + "action": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, + "then": "… NOOP", + "else": "… Detect repeated Nextcloud login failures" } ], "… Detect repeated Nextcloud login failures": [ @@ -251,7 +259,7 @@ { "action": "action_dailyReport", "args": { "level": "INFO", "message": "Failed login on {_HOSTNAME} by HTTPS" }, - "then": "… Detect repeated HTTPS failures" + "then": "… Do not ban local HTTP users" } ], "… Detect abnormal HTTP 404 errors": [ @@ -261,10 +269,18 @@ "open\\(\\) \"[^\"]*\\.(?:cgi|php|pl|py|sh)\" failed \\(2: No such file or directory\\), client: (?P[^,]+),", "Unable to open primary script: .*\\.(?:cgi|php|pl|py|sh) \\(No such file or directory[^,]+, client: (?P[^,]+)," ] }, - "then": "… Detect repeated HTTPS failures", + "then": "… Do not ban local HTTP users", "else": "… Immediate warning for connectivity errors" } ], + "… Do not ban local HTTP users": [ + { + "action": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, + "then": "… NOOP", + "else": "… Detect repeated HTTPS failures" + } + ], "… Detect repeated HTTPS failures": [ { "action": "action_counterRaise", @@ -366,7 +382,7 @@ "^imap-login: Disconnected \\(no auth attempts in [0-9]{2,} secs\\): user=<>, rip=(?P[^,]+),", "^imap-login: Disconnected: Too many invalid commands.*, rip=(?P[^,]+)," ] }, - "then": "… Detect repeated mail failures", + "then": "… Do not ban local mail users", "else": "… Detect failed IMAP logins" } ], @@ -388,14 +404,22 @@ { "action": "action_dailyReport", "args": { "level": "WARN", "message": "Failed login as {thatUser}@{_HOSTNAME} by IMAP" }, - "then": "… Detect repeated mail failures" + "then": "… Do not ban local mail users" } ], "… Report inexisting IMAP user": [ { "action": "action_dailyReport", "args": { "level": "INFO", "message": "Failed login as {thatUser}@{_HOSTNAME} by IMAP" }, - "then": "… Detect repeated mail failures" + "then": "… Do not ban local mail users" + } + ], + "… Do not ban local mail users": [ + { + "action": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, + "then": "… NOOP", + "else": "… Detect repeated mail failures" } ], "… Detect repeated mail failures": [ @@ -514,7 +538,7 @@ "^.{19} rejected [EH]{2}LO from (?:\\([^)]*\\) )?\\[(?P[^]]+)\\]: syntactically invalid", "\\[(?P[^ ]+)\\] dropped: too many nonmail commands" ] }, - "then": "… Detect repeated mail failures", + "then": "… Do not ban local mail users", "else": "… NOOP if PRIORITY 5+" } ], @@ -715,9 +739,9 @@ { "filter": "filter_pcreAny", "args": { "field": "MESSAGE", "re": [ - "^Failed password for (?P.*) from (?P(?!192\\.168\\.1\\.201 )[^ ]*) port", - "^Invalid user (?P.*) from (?P(?!192\\.168\\.1\\.201 )[^ ]*) port", - "^User (?P.*) from (?P(?!192\\.168\\.1\\.201 )[^ ]*) not allowed because not listed in AllowUsers$" + "^Failed password for (?P.*) from (?P[^ ]*) port", + "^Invalid user (?P.*) from (?P[^ ]*) port", + "^User (?P.*) from (?P[^ ]*) not allowed because not listed in AllowUsers$" ] }, "else": "… Forbid antiquated clients" }, @@ -733,24 +757,32 @@ { "action": "action_dailyReport", "args": { "level": "WARN", "message": "Failed login as {thatUser}@{_HOSTNAME} by SSH" }, - "then": "… Detect repeated SSH login failures" + "then": "… Do not ban local SSH users" } ], "… Report inexisting SSH user": [ { "action": "action_dailyReport", "args": { "level": "INFO", "message": "Failed login as {thatUser}@{_HOSTNAME} by SSH" }, - "then": "… Detect repeated SSH login failures" + "then": "… Do not ban local SSH users" } ], "… Forbid antiquated clients": [ { "filter": "filter_pcre", - "args": { "field": "MESSAGE", "re": "^Unable to negotiate with ((?!192\\.168\\.1\\.201 )[^ ]*) port", "save": [ "thatIP" ] }, - "then": "… Detect repeated SSH login failures", + "args": { "field": "MESSAGE", "re": "^Unable to negotiate with ([^ ]*) port", "save": [ "thatIP" ] }, + "then": "… Do not ban local SSH users", "else": "… NOOP if PRIORITY 6+" } ], + "… Do not ban local SSH users": [ + { + "action": "filter_inNetworks", + "args": { "field": "thatIP", "nets": [ "192.168.1.0/24", "fd00::/8" ] }, + "then": "… NOOP", + "else": "… Detect repeated SSH login failures" + } + ], "… Detect repeated SSH login failures": [ { "action": "action_counterRaise", diff --git a/pyruse/filters/filter_inNetworks.py b/pyruse/filters/filter_inNetworks.py new file mode 100644 index 0000000..28eaf59 --- /dev/null +++ b/pyruse/filters/filter_inNetworks.py @@ -0,0 +1,47 @@ +# pyruse is intended as a replacement to both fail2ban and epylog +# Copyright © 2017–2018 Y. Gablin +# Full licensing information in the LICENSE file, or gnu.org/licences/gpl-3.0.txt if the file is missing. +import socket +from functools import reduce +from pyruse import base + +class Filter(base.Filter): + ipReducer = lambda bits, byte: bits<<8 | byte + + def __init__(self, args): + super().__init__() + self.field = args["field"] + ip4Nets = [] + ip6Nets = [] + for net in args["nets"]: + if ":" in net: + ip6Nets.append(self._toNetAndMask(socket.AF_INET6, 128, net)) + else: + ip4Nets.append(self._toNetAndMask(socket.AF_INET, 32, net)) + self.ip4Nets = ip4Nets + self.ip6Nets = ip6Nets + + def filter(self, entry): + if self.field not in entry: + return False + ip = entry[self.field] + if ":" in ip: + return self._filter(socket.AF_INET6, ip, self.ip6Nets) + else: + return self._filter(socket.AF_INET, ip, self.ip4Nets) + + def _filter(self, family, ip, nets): + for (net, mask) in nets: + numericIP = self._numericIP(family, ip) + if numericIP & mask == net: + return True + return False + + def _toNetAndMask(self, family, bits, net): + ip, mask = net.split("/") + numericMask = ((1<