filter_inNetworks: check if IP is in one of the given networks
parent
fb6a69c40e
commit
c5135e847a
|
@ -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.
|
||||
|
|
|
@ -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<thatIP>[^,]+),",
|
||||
"Unable to open primary script: .*\\.(?:cgi|php|pl|py|sh) \\(No such file or directory[^,]+, client: (?P<thatIP>[^,]+),"
|
||||
] },
|
||||
"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<thatIP>[^,]+),",
|
||||
"^imap-login: Disconnected: Too many invalid commands.*, rip=(?P<thatIP>[^,]+),"
|
||||
] },
|
||||
"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<thatIP>[^]]+)\\]: syntactically invalid",
|
||||
"\\[(?P<thatIP>[^ ]+)\\] 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<thatUser>.*) from (?P<thatIP>(?!192\\.168\\.1\\.201 )[^ ]*) port",
|
||||
"^Invalid user (?P<thatUser>.*) from (?P<thatIP>(?!192\\.168\\.1\\.201 )[^ ]*) port",
|
||||
"^User (?P<thatUser>.*) from (?P<thatIP>(?!192\\.168\\.1\\.201 )[^ ]*) not allowed because not listed in AllowUsers$"
|
||||
"^Failed password for (?P<thatUser>.*) from (?P<thatIP>[^ ]*) port",
|
||||
"^Invalid user (?P<thatUser>.*) from (?P<thatIP>[^ ]*) port",
|
||||
"^User (?P<thatUser>.*) from (?P<thatIP>[^ ]*) 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",
|
||||
|
|
|
@ -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<<int(mask))-1)<<(bits-int(mask))
|
||||
numericIP = self._numericIP(family, ip)
|
||||
return numericIP & numericMask, numericMask
|
||||
|
||||
def _numericIP(self, family, ipString):
|
||||
return reduce(Filter.ipReducer, socket.inet_pton(family, ipString))
|
|
@ -0,0 +1,42 @@
|
|||
# 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.
|
||||
from pyruse.filters.filter_inNetworks import Filter
|
||||
|
||||
def whenIp4InNet4ThenTrue():
|
||||
assert Filter({"field": "ip", "nets": ["34.56.78.90/12"]}).filter({"ip": "34.48.0.1"})
|
||||
|
||||
def whenIp4NotInNet4ThenFalse():
|
||||
assert not Filter({"field": "ip", "nets": ["34.56.78.90/12"]}).filter({"ip": "34.47.255.254"})
|
||||
|
||||
def whenIp6InNet6ThenTrue():
|
||||
assert Filter({"field": "ip", "nets": ["2001:db8:1:1a0::/59"]}).filter({"ip": "2001:db8:1:1a0::1"})
|
||||
|
||||
def whenIp6NotInNet6ThenFalse():
|
||||
assert not Filter({"field": "ip", "nets": ["2001:db8:1:1a0::/59"]}).filter({"ip": "2001:db8:1:19f:ffff:ffff:ffff:fffe"})
|
||||
|
||||
def whenNumericIp6InNet4ThenFalse():
|
||||
assert not Filter({"field": "ip", "nets": ["34.56.78.90/12"]}).filter({"ip": "::2230:1"})
|
||||
|
||||
def whenNumericIp4InNet6ThenFalse():
|
||||
assert not Filter({"field": "ip", "nets": ["::2230:1/108"]}).filter({"ip": "34.48.0.1"})
|
||||
|
||||
def whenIpInOneNetworkThenTrue():
|
||||
assert Filter({"field": "ip", "nets": ["::2230:1/108", "10.0.0.0/8", "34.56.78.90/12", "2001:db8:1:1a0::/59"]}).filter({"ip": "34.48.0.1"})
|
||||
|
||||
def whenNoIpThenFalse():
|
||||
assert not Filter({"field": "ip", "nets": ["::2230:1/108", "10.0.0.0/8"]}).filter({"no_ip": "Hi!"})
|
||||
|
||||
def whenNoNetworkThenFalse():
|
||||
assert not Filter({"field": "ip", "nets": []}).filter({"ip": "34.48.0.1"})
|
||||
|
||||
def unitTests():
|
||||
whenIp4InNet4ThenTrue()
|
||||
whenIp4NotInNet4ThenFalse()
|
||||
whenIp6InNet6ThenTrue()
|
||||
whenIp6NotInNet6ThenFalse()
|
||||
whenNumericIp6InNet4ThenFalse()
|
||||
whenNumericIp4InNet6ThenFalse()
|
||||
whenIpInOneNetworkThenTrue()
|
||||
whenNoIpThenFalse()
|
||||
whenNoNetworkThenFalse()
|
|
@ -19,12 +19,13 @@ def main():
|
|||
conf = config.Config(os.curdir)
|
||||
|
||||
# Unit tests
|
||||
import filter_equals, filter_greaterOrEquals, filter_in, filter_lowerOrEquals, filter_pcre, filter_pcreAny, filter_userExists
|
||||
import filter_equals, filter_greaterOrEquals, filter_in, filter_inNetworks, filter_lowerOrEquals, filter_pcre, filter_pcreAny, filter_userExists
|
||||
import action_counterRaise, action_counterReset, action_dailyReport, action_email, action_nftBan
|
||||
|
||||
filter_equals.unitTests()
|
||||
filter_greaterOrEquals.unitTests()
|
||||
filter_in.unitTests()
|
||||
filter_inNetworks.unitTests()
|
||||
filter_lowerOrEquals.unitTests()
|
||||
filter_pcre.unitTests()
|
||||
filter_pcreAny.unitTests()
|
||||
|
|
Loading…
Reference in New Issue