Merge pull request 'v0.12.0' (#13) from develop into main

Reviewed-on: #13
This commit is contained in:
2026-08-16 14:33:52 +05:00
39 changed files with 937 additions and 108 deletions
+37
View File
@@ -1,3 +1,40 @@
## 0.13.0 (16.08.2026)
#### Русский
* Обновления критических ошибок:
* Исправлена ошибка, из-за которой неудачные попытки авторизации по SSH не обрабатывались, если указанного пользователя не существовало в системе.
* Исправлена ошибка, из-за которой неудачные попытки аутентификации по SSH-ключу не обрабатывались.
<br>**Примечание:** По умолчанию SSH-сервер не регистрирует неудачные попытки входа с использованием ключа. Чтобы включить отслеживание неудачных попыток входа с использованием ключа, необходимо добавить параметр `LogLevel VERBOSE` в настройки `sshd_config`.
* Добавлено: поддержка исключений IP-адресов в настройках обработки blocklist.
* Теперь можно добавлять параметр `exclude_ips` в конфигурационный файл `blocklists.toml` чтобы исключать определённые диапазоны IP-адресов из обработки. По умолчанию: `["127.0.0.1/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7",]`.
* Реализована простая проверка на наличия таблицы в NFTables.
* Добавлены данные для проверки наличия таблицы в NFTables в команде `kor-elf-shield status`.
* Добавлена новая группа параметров `[rulesGuard]` в файл `firewall.toml`:
* `enabled` — Включает мониторинг правил NFTables. По умолчанию: `true`.
* `notifications` - Включает уведомления в случае проблем с правилами NFTables. По умолчанию: `true`.
* `recovery` - Восстанавливает правила NFTables в случае проблем. По умолчанию: `true`.
* `interval` - Интервал проверки правил NFTables в секундах. По умолчанию: `3600`.
* Добавлена поддержка разделения `patterns` на партиции в настройках `analyzer.toml`.
* Теперь правила анализа логов и защиты от перебора пароля могут обрабатывать шаблоны по партициям.
* Это помогает гибче группировать шаблоны и снижать пересечения между независимыми наборами правил.
***
#### English
* Critical bug fixes:
* Fixed an issue where failed SSH login attempts were not processed if the specified user did not exist on the system.
* Fixed a bug where unsuccessful SSH key authentication attempts were not processed.
<br>**Note:** By default, the SSH server does not log unsuccessful login attempts using a key. To enable tracking of unsuccessful login attempts using a key, you must add the `LogLevel VERBOSE` parameter to the `sshd_config` settings.
* Added: Support for IP address exclusions in blocklist processing settings.
* You can now add the `exclude_ips` parameter to the `blocklists.toml` configuration file to exclude specific IP address ranges from processing. Default: `["127.0.0.1/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7",]`.
* Implemented a simple check for the presence of a table in NFTables.
* Added check data to `kor-elf-shield status` command for the presence of a table in NFTables.
* Added a new `[rulesGuard]` parameter group to the `firewall.toml` file:
* `enabled` - Enables NFTables rules monitoring. Default: `true`.
* `notifications` - Enables notifications in case of problems with NFTables rules. Default: `true`.
* `recovery` - Recovers NFTables rules in case of problems. Default: `true`.
* `interval` - NFTables rules checking interval in seconds. Default: `3600`.
* Added partitioning support for `patterns` in the `analyzer.toml` configuration.
* Log analysis and brute-force protection rules can now process patterns by partitions.
* This helps organize patterns more flexibly and reduces overlap between independent rule sets.
***
## 0.12.0 (17.06.2026)
#### Русский
* Добавлено: отображение номера уведомления в групповых уведомлениях.
+32
View File
@@ -903,6 +903,38 @@ http://www.apache.org/licenses/LICENSE-2.0
--------------------------------------------------------------------------------
golang.org/x/crypto
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
golang.org/x/sys
Copyright 2009 The Go Authors.
+38 -1
View File
@@ -68,9 +68,15 @@ rate_limit_reset_period = 86400
###
# Включает защиту от перебора пароля от ssh.
# !!! Примечание: По умолчанию SSH-сервер не регистрирует неудачные попытки входа с использованием ключа.
# Чтобы включить отслеживание неудачных попыток входа с использованием ключа, необходимо добавить параметр
# "LogLevel VERBOSE" в настройки sshd_config !!!
# По умолчанию: true
# ***
# Enables protection against brute-force attacks against ssh.
# !!! Note: By default, the SSH server does not log unsuccessful login attempts using a key.
# To enable tracking of unsuccessful login attempts using a key, you must add the "LogLevel VERBOSE" parameter
# to the sshd_config settings !!!
# Default: true
###
ssh_enable = true
@@ -207,7 +213,14 @@ ssh_group = ""
# [[bruteForceProtection.rules.patterns.values]]
# name = "Пользователь"
# value = 1
#
# [bruteForceProtection.rules.patterns.partition] # (необязательный параметр) если надо разделить по ключевым словам
# value = 1 # Номер поля, по которому мы получаем ключевое слова
# trim = true # Убирает у ключевого слова пробелы вначале и в конце
# lower_case = true # Ключевое слово приводит к нижнему регистру
# [bruteForceProtection.rules.patterns.partition.type] # (необязательный параметр)
# type = "only" # Допустимые значения "only" или "except"
# keywords = ["root", "user"] # Указываем ключевые слова
# partitioned = false # Если нужно, чтобы каждое ключевое слово считалось независимо от друг-друга, то указываем true
# ***
# Specify the log settings that need to be monitored to protect against password brute-force attacks.
# Examples:
@@ -233,6 +246,14 @@ ssh_group = ""
# [[bruteForceProtection.rules.patterns.values]]
# name = "User"
# value = 1
# [bruteForceProtection.rules.patterns.partition] # (optional) if you want to split by keywords
# value = 1 # Field number by which to get the keyword
# trim = true # Removes leading and trailing spaces from the keyword
# lower_case = true # Converts the keyword to lowercase
# [bruteForceProtection.rules.patterns.partition.type] # (optional)
# type = "only" # Acceptable values are "only" or "except"
# keywords = ["root", "user"] # Specify the keywords
# partitioned = false # If you want each keyword to be considered independently, specify true
###
###############################################################################
@@ -438,6 +459,14 @@ notify = true
# name = "IP"
# value = 3
# type = "ip"
# [logAlert.rules.patterns.partition] # (необязательный параметр) если надо разделить по ключевым словам
# value = 2 # Номер поля, по которому мы получаем ключевое слова
# trim = true # Убирает у ключевого слова пробелы вначале и в конце
# lower_case = true # Ключевое слово приводит к нижнему регистру
# [logAlert.rules.patterns.partition.type] # (необязательный параметр)
# type = "only" # Допустимые значения "only" или "except"
# keywords = ["root", "user"] # Указываем ключевые слова
# partitioned = false # Если нужно, чтобы каждое ключевое слово считалось независимо от друг-друга, то указываем true
#
# ***
# Specify the log settings to monitor for notifications.
@@ -463,4 +492,12 @@ notify = true
# name = "IP"
# value = 3
# type = "ip"
# [logAlert.rules.patterns.partition] # (optional) if you want to split by keywords
# value = 2 # Field number by which we get the keyword
# trim = true # Removes leading and trailing spaces from the keyword
# lower_case = true # Converts the keyword to lowercase
# [logAlert.rules.patterns.partition.type] # (optional)
# type = "only" # Acceptable values are "only" or "except"
# keywords = ["root", "user"] # Specify the keywords
# partitioned = false # If you want each keyword to be counted independently, specify true
###
+32
View File
@@ -17,6 +17,38 @@
###
enabled = false
###
# Исключить IP-адреса из полученных списков.
# Допустимые форматы: "192.168.1.1", "192.168.1.0/24", "192.168.1.0-192.168.1.255"
# По умолчанию: [
# "127.0.0.1/8",
# "10.0.0.0/8",
# "172.16.0.0/12",
# "192.168.0.0/16",
# "::1/128",
# "fc00::/7",
# ]
# ***
# Exclude IP addresses from retrieved lists.
# Acceptable formats: "192.168.1.1", "192.168.1.0/24", "192.168.1.0-192.168.1.255"
# Default: [
# "127.0.0.1/8",
# "10.0.0.0/8",
# "172.16.0.0/12",
# "192.168.0.0/16",
# "::1/128",
# "fc00::/7",
# ]
###
exclude_ips = [
"127.0.0.1/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fc00::/7",
]
###
# Spamhaus Don't Route Or Peer Lists (DROP IPv4)
# Details: https://www.spamhaus.org/blocklists/do-not-route-or-peer/
+43
View File
@@ -621,3 +621,46 @@ chain_output_name = "output"
# Default: "forward"
###
chain_forward_name = "forward"
###############################################################################
# РАЗДЕЛ:Проверка и восстановление правил брандмауэра
# ***
# SECTION:Checking and restoring firewall rules
###############################################################################
[rulesGuard]
###
# Включает мониторинг правил NFTables.
# По умолчанию: true
# ***
# Enables monitoring of NFTables rules.
# Default: true
###
enabled = true
###
# Включает уведомления в случае возникновения проблем с правилами NFTables.
# По умолчанию: true
# ***
# Enables notifications if there is a problem with NFTables rules.
# Default: true
###
notifications = true
###
# Если возникла проблема с правилами NFTables, следует ли перезапустить настройки правил NFTables.
# По умолчанию: true
# ***
# If there is a problem with the NFTables rules, whether to restart the NFTables rules settings.
# Default: true
###
recovery = true
###
# Указать интервал в секундах по которому будет проверяться на наличия правил NFTables.
# По умолчанию: 3600
# ***
# Specify the interval in seconds at which to check for NFTables rules.
# Default: 3600
###
interval = 3600
+14 -13
View File
@@ -1,35 +1,36 @@
module git.kor-elf.net/kor-elf-shield/kor-elf-shield
go 1.25
go 1.25.0
require (
git.kor-elf.net/kor-elf-shield/blocklist v1.1.0
git.kor-elf.net/kor-elf-shield/blocklist v1.2.0
git.kor-elf.net/kor-elf-shield/geoip2 v0.1.2
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.2.1
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/nxadm/tail v1.4.11
github.com/spf13/viper v1.21.0
github.com/urfave/cli/v3 v3.6.2
github.com/wneessen/go-mail v0.7.2
go.etcd.io/bbolt v1.4.3
go.uber.org/zap v1.27.1
golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0
github.com/urfave/cli/v3 v3.10.1
github.com/wneessen/go-mail v0.8.1
go.etcd.io/bbolt v1.5.0
go.uber.org/zap v1.28.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.41.0
)
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/oschwald/geoip2-golang/v2 v2.1.0 // indirect
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/oschwald/geoip2-golang/v2 v2.3.0 // indirect
github.com/oschwald/maxminddb-golang/v2 v2.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.55.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
+27
View File
@@ -1,5 +1,7 @@
git.kor-elf.net/kor-elf-shield/blocklist v1.1.0 h1:NS8be3TFBsUn+ft3oG5sAD56iJTGOkFH6GgjepEnS0s=
git.kor-elf.net/kor-elf-shield/blocklist v1.1.0/go.mod h1:nNbQux5vbuoCa3wMiC2QsLb4tO1JLCssGzdljizcJUs=
git.kor-elf.net/kor-elf-shield/blocklist v1.2.0 h1:oj4P4N5sa0kh2Zob0Mf/GrjqNAQrZjuX9/RoDx3UUGk=
git.kor-elf.net/kor-elf-shield/blocklist v1.2.0/go.mod h1:nNbQux5vbuoCa3wMiC2QsLb4tO1JLCssGzdljizcJUs=
git.kor-elf.net/kor-elf-shield/geoip2 v0.1.2 h1:/J9U+h9H92hW6TtwCznkRANqhX5kvBpN4uV7xDbwXpM=
git.kor-elf.net/kor-elf-shield/geoip2 v0.1.2/go.mod h1:ULMUjpd2I9ikkDDE69IlpKT4vR2/nlYT0cqoR2T95sM=
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.2.1 h1:B5u1uCYyrDlDlCSA03o/Djt/T0A3SgCeFsfZkq25Hwg=
@@ -13,6 +15,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -27,10 +31,16 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
github.com/oschwald/geoip2-golang/v2 v2.3.0 h1:hT8/BT137lPJXq0DXwGQUS228k8pEhgBRJ1B70eqyAk=
github.com/oschwald/geoip2-golang/v2 v2.3.0/go.mod h1:tHUYg65ssvQSSzSCkiFR6LWJPYOvSw/85JiBp8kXz0U=
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
github.com/oschwald/maxminddb-golang/v2 v2.5.0 h1:WvEHCE8HwFS5pKWhW8nvvRxNzczuRUOGBLn2L03VlEQ=
github.com/oschwald/maxminddb-golang/v2 v2.5.0/go.mod h1:EBnvLGgY+aSckqcgyfB5LPDviqaWdMZPBDwu8c2jJbs=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@@ -51,25 +61,42 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -29,7 +29,17 @@ func NewBruteForceProtectionSSH(isNotify bool, notifyCooldown int, notifyEvery i
Patterns: []brute_force_protection.RegexPattern{
{
Regexp: regular_expression.NewLazyRegexp(`^Failed password for (\S+) from (\S+) port \S+`),
Regexp: regular_expression.NewLazyRegexp(`^Failed (password|publickey) for (invalid user |illegal user )?(\S*) from (\S+)( port \S+ \S+\s*)`),
Values: []brute_force_protection.PatternValue{
{
Name: i18n.Lang.T("user"),
Value: 3,
},
},
IP: 4,
},
{
Regexp: regular_expression.NewLazyRegexp(`^Invalid user (\S*) from (\S+) port (\S+)`),
Values: []brute_force_protection.PatternValue{
{
Name: i18n.Lang.T("user"),
@@ -1,6 +1,9 @@
package brute_force_protection
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/partition"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
)
type Rule struct {
Name string
@@ -15,9 +18,10 @@ type Rule struct {
}
type RegexPattern struct {
Regexp *regular_expression.LazyRegexp
Values []PatternValue
IP uint8
Regexp *regular_expression.LazyRegexp
Values []PatternValue
IP uint8
Partition *partition.PatternPartition
}
type RateLimit struct {
+4 -2
View File
@@ -7,6 +7,7 @@ import (
"unicode"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/partition"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
)
@@ -112,8 +113,9 @@ type AlertRule struct {
}
type AlertRegexPattern struct {
Regexp *regular_expression.LazyRegexp
Values []PatternValue
Regexp *regular_expression.LazyRegexp
Values []PatternValue
Partition *partition.PatternPartition
}
type PatternValue struct {
@@ -0,0 +1,29 @@
package partition
type exceptType struct {
keywords []string
partitioned bool
}
func NewExceptType(
keywords []string,
partitioned bool,
) PatternPartitionType {
return &exceptType{
keywords: keywords,
partitioned: partitioned,
}
}
func (o *exceptType) IsPartitioned() bool {
return o.partitioned
}
func (o *exceptType) Accepts(text string) (bool, string) {
for _, keyword := range o.keywords {
if keyword == text {
return false, text
}
}
return true, text
}
@@ -0,0 +1,29 @@
package partition
type onlyType struct {
keywords []string
partitioned bool
}
func NewOnlyType(
keywords []string,
partitioned bool,
) PatternPartitionType {
return &onlyType{
keywords: keywords,
partitioned: partitioned,
}
}
func (o *onlyType) IsPartitioned() bool {
return o.partitioned
}
func (o *onlyType) Accepts(text string) (bool, string) {
for _, keyword := range o.keywords {
if keyword == text {
return true, text
}
}
return false, text
}
@@ -0,0 +1,40 @@
package partition
import "strings"
type PatternPartitionType interface {
Accepts(text string) (bool, string)
IsPartitioned() bool
}
type PatternPartition struct {
Value uint8
Type PatternPartitionType
Normalize func(keyword string) string
}
type Normalize interface {
Normalize(text string) string
}
type normalize struct {
Trim bool
LowerCase bool
}
func NewNormalize(trim, lowerCase bool) Normalize {
return &normalize{
Trim: trim,
LowerCase: lowerCase,
}
}
func (p *normalize) Normalize(text string) string {
if p.LowerCase {
text = strings.ToLower(text)
}
if p.Trim {
text = strings.TrimSpace(text)
}
return text
}
+31 -5
View File
@@ -26,8 +26,9 @@ type alert struct {
}
type alertAnalyzeRuleReturn struct {
found bool
fields []*regexField
found bool
fields []*regexField
partition *string
}
type alertNotify struct {
@@ -68,7 +69,7 @@ func (a *alert) Analyze(entry *Entry) {
alertNumber := uint64(0)
messages := []string{}
if rule.Group != nil {
alertGroup, err := a.alertGroupService.Analyze(rule.Group, entry.Time, entry.Message)
alertGroup, err := a.alertGroupService.Analyze(rule.Group, entry.Time, entry.Message, result.partition)
if err != nil {
a.logger.Error(fmt.Sprintf("Failed to analyze alert group: %s", err))
continue
@@ -102,8 +103,9 @@ func (a *alert) ClearDBData() error {
func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyzeRuleReturn {
result := alertAnalyzeRuleReturn{
found: false,
fields: []*regexField{},
found: false,
fields: []*regexField{},
partition: nil,
}
for _, pattern := range rule.Patterns {
@@ -116,6 +118,27 @@ func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyze
idx := re.FindStringSubmatchIndex(message)
if idx != nil {
if pattern.Partition != nil {
start, end, err := getValueStartEndByRegexIndex(int(pattern.Partition.Value), idx)
if err != nil {
a.logger.Error(fmt.Sprintf("alert. Failed to get partition value: %s", err))
continue
}
partition := pattern.Partition.Normalize(message[start:end])
if pattern.Partition.Type != nil {
if accepts, partitionCode := pattern.Partition.Type.Accepts(partition); !accepts {
continue
} else {
partition = partitionCode
}
if pattern.Partition.Type.IsPartitioned() {
result.partition = &partition
}
} else {
result.partition = &partition
}
}
for _, value := range pattern.Values {
start, end, err := getValueStartEndByRegexIndex(int(value.Value), idx)
if err != nil {
@@ -126,6 +149,9 @@ func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyze
}
if len(pattern.Values) != len(result.fields) {
a.logger.Error(fmt.Sprintf("alert. analyzeRule len(pattern.Values) != len(result.fields): %d != %d", len(pattern.Values), len(result.fields)))
result.fields = []*regexField{}
result.partition = nil
continue
}
@@ -12,7 +12,7 @@ import (
)
type Group interface {
Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message string) (AnalysisResult, error)
Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message string, partition *string) (AnalysisResult, error)
ClearDBData() error
}
@@ -34,14 +34,14 @@ func NewGroup(alertGroupRepository repository.AlertGroupRepository, logger log.L
}
}
func (g *group) Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message string) (AnalysisResult, error) {
func (g *group) Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message string, partition *string) (AnalysisResult, error) {
analysisResult := AnalysisResult{
Alerted: false,
}
g.logger.Debug(fmt.Sprintf("Analyzing alert group %s", alertGroup.Name))
err := g.alertGroupRepository.Update(alertGroup.Name, func(entityAlertGroup *entity.AlertGroup) (*entity.AlertGroup, error) {
err := g.alertGroupRepository.Update(alertGroup.Name, partition, func(entityAlertGroup *entity.AlertGroup) (*entity.AlertGroup, error) {
rateLimit, err := alertGroup.RateLimit(entityAlertGroup.CurrentLevelTriggerCount)
if err != nil {
return entityAlertGroup, err
@@ -34,9 +34,10 @@ type bruteForceProtection struct {
}
type bruteForceProtectionAnalyzeRuleReturn struct {
found bool
fields []*regexField
ip net.IP
found bool
fields []*regexField
ip net.IP
partition *string
}
type bruteForceProtectionNotify struct {
@@ -88,7 +89,7 @@ func (p *bruteForceProtection) Analyze(entry *Entry) {
continue
}
groupResult, err := p.groupService.Analyze(rule.Group, entry.Time, result.ip, entry.Message)
groupResult, err := p.groupService.Analyze(rule.Group, entry.Time, result.ip, entry.Message, result.partition)
if err != nil {
p.logger.Error(fmt.Sprintf("Failed to analyze brute force protection group: %s", err))
continue
@@ -198,9 +199,10 @@ func (p *bruteForceProtection) handleBlockIPWithPorts(
func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, message string) bruteForceProtectionAnalyzeRuleReturn {
result := bruteForceProtectionAnalyzeRuleReturn{
found: false,
fields: []*regexField{},
ip: nil,
found: false,
fields: []*regexField{},
ip: nil,
partition: nil,
}
for _, pattern := range rule.Patterns {
@@ -216,14 +218,35 @@ func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, me
start, end, err := getValueStartEndByRegexIndex(int(pattern.IP), idx)
if err != nil {
p.logger.Error(fmt.Sprintf("Failed to get ip value: %s", err))
return result
continue
}
ipText := message[start:end]
result.ip = net.ParseIP(ipText)
if result.ip == nil {
ip := net.ParseIP(ipText)
if ip == nil {
p.logger.Error(fmt.Sprintf("Failed to parse ip: %s", ipText))
return bruteForceProtectionAnalyzeRuleReturn{
found: false,
continue
}
result.ip = ip
if pattern.Partition != nil {
start, end, err := getValueStartEndByRegexIndex(int(pattern.Partition.Value), idx)
if err != nil {
p.logger.Error(fmt.Sprintf("brute_force_protection. Failed to get partition value: %s", err))
result.ip = nil
continue
}
partition := pattern.Partition.Normalize(message[start:end])
if pattern.Partition.Type != nil {
if accepts, partitionCode := pattern.Partition.Type.Accepts(partition); !accepts {
continue
} else {
partition = partitionCode
}
if pattern.Partition.Type.IsPartitioned() {
result.partition = &partition
}
} else {
result.partition = &partition
}
}
@@ -237,6 +260,10 @@ func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, me
}
if len(pattern.Values) != len(result.fields) {
p.logger.Error(fmt.Sprintf("brute_force_protection. analyzeRule len(pattern.Values) != len(result.fields): %d != %d", len(pattern.Values), len(result.fields)))
result.fields = []*regexField{}
result.partition = nil
result.ip = nil
continue
}
@@ -13,7 +13,7 @@ import (
)
type Group interface {
Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message string) (AnalysisResult, error)
Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message string, partition *string) (AnalysisResult, error)
ClearDBData() error
}
@@ -37,14 +37,14 @@ func NewGroup(groupRepository repository.BruteForceProtectionGroupRepository, lo
}
}
func (g *group) Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message string) (AnalysisResult, error) {
func (g *group) Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message string, partition *string) (AnalysisResult, error) {
analysisResult := AnalysisResult{
Block: false,
}
g.logger.Debug(fmt.Sprintf("Analyzing brute force protection group %s IP %s", group.Name, ip.String()))
err := g.groupRepository.Update(group.Name, ip, func(entityGroup *entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error) {
err := g.groupRepository.Update(group.Name, ip, partition, func(entityGroup *entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error) {
rateLimit, err := group.RateLimit(entityGroup.CurrentLevelTriggerCount)
if err != nil {
return entityGroup, err
+24 -10
View File
@@ -15,6 +15,7 @@ import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/info"
@@ -33,16 +34,17 @@ type Daemon interface {
}
type daemon struct {
info info.Info
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
notifications notifications.Notifications
analyzer analyzer.Analyzer
docker docker_monitor.Docker
blocklist blocklist.Blocklist
geoIPService geoip.GeoIP
info info.Info
pidFile pidfile.PidFile
socket socket.Socket
logger log.Logger
firewall firewall.API
firewallRulesGuard guard.RulesGuard
notifications notifications.Notifications
analyzer analyzer.Analyzer
docker docker_monitor.Docker
blocklist blocklist.Blocklist
geoIPService geoip.GeoIP
stopCh chan struct{}
}
@@ -59,6 +61,10 @@ func (d *daemon) Run(ctx context.Context, isTesting bool, testingInterval uint16
return err
}
d.firewall.SavesRules()
d.firewallRulesGuard.Run(d.info, ctx)
defer func() {
_ = d.firewallRulesGuard.Close()
}()
if err := d.pidFile.Create(); err != nil {
return err
@@ -162,8 +168,15 @@ func (d *daemon) socketCommand(command string, args map[string]string, socket so
var m runtime.MemStats
runtime.ReadMemStats(&m)
hasRules, err := d.firewall.HasRules()
nftables := "\033[32mOk\033[0m"
if err != nil || !hasRules {
nftables = "\033[31mError\033[0m"
}
text := fmt.Sprintf(
"ok\n\n***\n"+
"NFTables: %s\n"+
"Version: %s\n"+
"BuiltWith: %s\n"+
"Uptime: %s\n"+
@@ -174,6 +187,7 @@ func (d *daemon) socketCommand(command string, args map[string]string, socket so
"HeapSys: %s\n"+
"NumGC: %d\n"+
"***\n",
nftables,
d.info.Version(),
d.info.BuiltWith(),
format.HumanDuration(d.info.Uptime()),
+25 -3
View File
@@ -1,6 +1,7 @@
package repository
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -11,7 +12,7 @@ import (
)
type AlertGroupRepository interface {
Update(name string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error
Update(name string, partition *string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error
Clear() error
}
@@ -27,7 +28,7 @@ func NewAlertGroupRepository(appDB *bbolt.DB) AlertGroupRepository {
}
}
func (r *alertGroupRepository) Update(name string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error {
func (r *alertGroupRepository) Update(name string, partition *string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error {
entityAlertGroup := &entity.AlertGroup{}
entityAlertGroup.Reset()
@@ -36,7 +37,10 @@ func (r *alertGroupRepository) Update(name string, f func(*entity.AlertGroup) (*
if err != nil {
return err
}
key := []byte(name)
key, err := keyGroup(name, partition)
if err != nil {
return err
}
group := b.Get(key)
if group != nil {
@@ -70,3 +74,21 @@ func (r *alertGroupRepository) Clear() error {
return err
})
}
func keyGroup(groupID string, partition *string) ([]byte, error) {
if len(groupID) == 0 {
return nil, fmt.Errorf("group id cannot be empty")
}
if partition == nil {
return []byte(groupID), nil
}
partitionHash := sha256.Sum256([]byte(*partition))
k := make([]byte, 0, len(groupID)+1+len(partitionHash))
k = append(k, groupID...)
k = append(k, 0x00)
k = append(k, partitionHash[:]...)
return k, nil
}
@@ -1,6 +1,7 @@
package repository
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -12,7 +13,7 @@ import (
)
type BruteForceProtectionGroupRepository interface {
Update(name string, ip net.IP, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error
Update(name string, ip net.IP, partition *string, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error
Clear() error
}
@@ -28,7 +29,7 @@ func NewBruteForceProtectionGroupRepository(appDB *bbolt.DB) BruteForceProtectio
}
}
func (r *bruteForceProtectionGroupRepository) Update(name string, ip net.IP, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error {
func (r *bruteForceProtectionGroupRepository) Update(name string, ip net.IP, partition *string, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error {
entityGroup := &entity.BruteForceProtectionGroup{}
entityGroup.Reset()
@@ -37,7 +38,7 @@ func (r *bruteForceProtectionGroupRepository) Update(name string, ip net.IP, f f
if err != nil {
return err
}
key, err := keyGroupIP(name, ip)
key, err := keyGroupIP(name, ip, partition)
if err != nil {
return err
}
@@ -75,7 +76,7 @@ func (r *bruteForceProtectionGroupRepository) Clear() error {
})
}
func keyGroupIP(groupID string, ip net.IP) ([]byte, error) {
func keyGroupIP(groupID string, ip net.IP, partition *string) ([]byte, error) {
if ip == nil {
return nil, fmt.Errorf("ip cannot be nil")
}
@@ -95,6 +96,17 @@ func keyGroupIP(groupID string, ip net.IP) ([]byte, error) {
ipAddr = ip.To16()
}
if partition != nil {
partitionHash := sha256.Sum256([]byte(*partition))
k := make([]byte, 0, len(groupID)+1+len(ipAddr)+1+len(partitionHash))
k = append(k, groupID...)
k = append(k, 0x00)
k = append(k, ipAddr...)
k = append(k, 0x00)
k = append(k, partitionHash[:]...)
return k, nil
}
k := make([]byte, 0, len(groupID)+1+len(ipAddr))
k = append(k, groupID...)
k = append(k, 0x00)
+7
View File
@@ -48,6 +48,9 @@ type API interface {
// DockerSupport Return true if docker support
DockerSupport() bool
// HasRules Return true if firewall has rules
HasRules() (bool, error)
}
type firewall struct {
@@ -208,6 +211,10 @@ func (f *firewall) SavesRules() {
f.logger.Info("Save nftables rules")
}
func (f *firewall) HasRules() (bool, error) {
return f.table.HasRules()
}
func (f *firewall) BlockIP(blockIP blocking.BlockIP) (bool, error) {
isBanned, err := f.blockingService.BlockIP(blockIP)
@@ -0,0 +1,8 @@
package config
type GuardConfig struct {
Enable bool
Notifications bool
Recovery bool
Interval uint32
}
+143
View File
@@ -0,0 +1,143 @@
package guard
import (
"context"
"fmt"
"sync"
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/info"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
)
type firewallGuardTarget interface {
HasRules() (bool, error)
Reload(daemonInfo info.Info) error
}
type RulesGuard interface {
Run(daemonInfo info.Info, ctx context.Context)
Close() error
}
type rulesGuard struct {
config *config.GuardConfig
firewall firewallGuardTarget
notify notifications.Notifications
logger log.Logger
mu sync.Mutex
cancel context.CancelFunc
}
func NewRulesGuard(config *config.GuardConfig, firewall firewallGuardTarget, notify notifications.Notifications, logger log.Logger) RulesGuard {
return &rulesGuard{
config: config,
firewall: firewall,
notify: notify,
logger: logger,
}
}
func (g *rulesGuard) Run(daemonInfo info.Info, ctx context.Context) {
if !g.config.Enable {
g.logger.Debug("firewall rules guard is disabled")
return
}
g.logger.Debug("firewall rules guard is enabled")
guardCtx, cancel := context.WithCancel(ctx)
g.mu.Lock()
g.cancel = cancel
g.mu.Unlock()
go g.run(daemonInfo, guardCtx)
}
func (g *rulesGuard) Close() error {
g.mu.Lock()
cancel := g.cancel
g.cancel = nil
g.mu.Unlock()
if cancel != nil {
cancel()
}
return nil
}
func (g *rulesGuard) run(daemonInfo info.Info, ctx context.Context) {
interval := time.Duration(g.config.Interval) * time.Second
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
g.checkRules(daemonInfo)
case <-ctx.Done():
g.logger.Debug("firewall rules guard stopped")
return
}
}
}
func (g *rulesGuard) checkRules(daemonInfo info.Info) {
hasRules, err := g.firewall.HasRules()
if err != nil {
g.logger.Error(fmt.Sprintf("failed to check firewall rules: %s", err))
if g.config.Notifications {
message := notifications.Message{
Subject: i18n.Lang.T("firewall_rules_not_found"),
Body: i18n.Lang.T("firewall_rules_not_found_body_check_error", map[string]interface{}{
"Error": err.Error(),
}),
}
g.notify.SendAsync(message)
}
return
}
if hasRules {
g.logger.Debug("firewall rules exists")
return
}
g.logger.Warn("firewall rules not found")
if g.config.Recovery {
if err := g.firewall.Reload(daemonInfo); err != nil {
g.logger.Error(fmt.Sprintf("failed to recover firewall rules: %s", err))
if g.config.Notifications {
message := notifications.Message{
Subject: i18n.Lang.T("firewall_rules_not_found"),
Body: i18n.Lang.T("firewall_rules_not_found_body_recover_error", map[string]interface{}{
"Error": err.Error(),
}),
}
g.notify.SendAsync(message)
}
return
}
g.logger.Warn("firewall rules recovered")
if g.config.Notifications {
message := notifications.Message{
Subject: i18n.Lang.T("firewall_rules_not_found"),
Body: i18n.Lang.T("firewall_rules_not_found_body_recover_success"),
}
g.notify.SendAsync(message)
}
} else if g.config.Notifications {
message := notifications.Message{
Subject: i18n.Lang.T("firewall_rules_not_found"),
Body: i18n.Lang.T("firewall_rules_not_found_body"),
}
g.notify.SendAsync(message)
}
}
@@ -1,6 +1,8 @@
package table
import (
"strings"
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/nft"
@@ -11,6 +13,9 @@ type Table interface {
Clear() error
DockerChains() firewall.NFTDockerChains
BlockList() BlockList
// HasRules Return true if the table has rules
HasRules() (bool, error)
}
type BlockList interface {
@@ -55,6 +60,45 @@ func (t *table) BlockList() BlockList {
return t.blockList
}
func (t *table) HasRules() (bool, error) {
if exists, err := t.exists(); err != nil {
return false, err
} else if !exists {
return false, nil
}
args := []string{"list", "table", t.family.String(), t.name}
output, err := t.nft.NFT().Command().RunWithOutput(args...)
if err != nil {
return false, err
}
openBrace := strings.Index(output, "{")
closeBrace := strings.LastIndex(output, "}")
if openBrace == -1 || closeBrace == -1 || closeBrace <= openBrace {
return false, nil
}
content := strings.TrimSpace(output[openBrace+1 : closeBrace])
return content != "", nil
}
func (t *table) exists() (bool, error) {
args := []string{"list", "tables", t.family.String()}
output, err := t.nft.NFT().Command().RunWithOutput(args...)
if err != nil {
return false, err
}
for _, line := range strings.Split(output, "\n") {
if strings.TrimSpace(line) == "table "+t.family.String()+" "+t.name {
return true, nil
}
}
return false, nil
}
type blockList struct {
listIP block.ListIP
listIPWithPort block.ListIPWithPort
+9 -7
View File
@@ -4,14 +4,16 @@ import (
analyzerConfig "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db"
firewallConfig "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/config"
GuardConfig "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard/config"
)
type DaemonOptions struct {
PathPidFile string
PathSocketFile string
DataDir string
PathNftables string
ConfigFirewall firewallConfig.Config
ConfigAnalyzer analyzerConfig.Config
Repositories db.Repositories
PathPidFile string
PathSocketFile string
DataDir string
PathNftables string
ConfigFirewall firewallConfig.Config
ConfigFirewallGuard GuardConfig.GuardConfig
ConfigAnalyzer analyzerConfig.Config
Repositories db.Repositories
}
+13 -10
View File
@@ -10,6 +10,7 @@ import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
firewall2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/info"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
@@ -57,20 +58,22 @@ func NewDaemon(
if err != nil {
return nil, err
}
firewallRulesGuard := guard.NewRulesGuard(&opts.ConfigFirewallGuard, firewall, notifications, logger)
blockService := brute_force_protection_group.NewBlockService(firewall.BlockIP, firewall.BlockIPWithPorts)
analyzerService := analyzer.New(opts.ConfigAnalyzer, blockService, opts.Repositories, logger, notifications, geoIPService.Info)
return &daemon{
info: info,
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
notifications: notifications,
analyzer: analyzerService,
docker: docker,
blocklist: blocklist,
geoIPService: geoIPService,
info: info,
pidFile: pidFile,
socket: sock,
logger: logger,
firewall: firewall,
firewallRulesGuard: firewallRulesGuard,
notifications: notifications,
analyzer: analyzerService,
docker: docker,
blocklist: blocklist,
geoIPService: geoIPService,
}, nil
}
+7 -1
View File
@@ -86,5 +86,11 @@
"alert.bruteForceProtection.ssh.message": "An attempt to brute-force SSH was detected.",
"alert.bruteForceProtection.group._default.message": "Default group.",
"cmd.error": "Command error: {{.Error}}"
"cmd.error": "Command error: {{.Error}}",
"firewall_rules_not_found": "No firewall rules found",
"firewall_rules_not_found_body": "No firewall rules found. The \"Restore Rules\" setting is disabled. You are currently unprotected!!!",
"firewall_rules_not_found_body_recover_error": "Firewall rules not found. We couldn't restore firewall rules. You are currently unprotected!!! Error restoring firewall rules: {{.Error}}",
"firewall_rules_not_found_body_recover_success": "No firewall rules were found. However, the firewall rules were successfully restored!",
"firewall_rules_not_found_body_check_error": "Error checking firewall rules: {{.Error}}"
}
+7 -1
View File
@@ -86,5 +86,11 @@
"alert.bruteForceProtection.ssh.message": "SSH-ті күштеп қолдану әрекеті анықталды.",
"alert.bruteForceProtection.group._default.message": "Әдепкі топ.",
"cmd.error": "Команда қатесі: {{.Error}}"
"cmd.error": "Команда қатесі: {{.Error}}",
"firewall_rules_not_found": "Брандмауэр ережелері табылмады",
"firewall_rules_not_found_body": "Брандмауэр ережелері табылмады. \"Ережелерді қалпына келтіру\" опциясы өшірілген. Сіз қазіргі уақытта қорғалмағансыз!!!",
"firewall_rules_not_found_body_recover_error": "Брандмауэр ережелері табылмады. Брандмауэр ережелерін қалпына келтіре алмадық. Сіз қазіргі уақытта қорғалмағансыз!!! Брандмауэр ережелерін қалпына келтіруде қате пайда болды: {{.Error}}",
"firewall_rules_not_found_body_recover_success": "Брандмауэр ережелері табылмады. Дегенмен, брандмауэр ережелері сәтті қалпына келтірілді!",
"firewall_rules_not_found_body_check_error": "Брандмауэр ережелерін тексеру кезінде қате пайда болды: {{.Error}}"
}
+7 -1
View File
@@ -86,5 +86,11 @@
"alert.bruteForceProtection.ssh.message": "Обнаружена попытка атаки на SSH методом перебора паролей.",
"alert.bruteForceProtection.group._default.message": "Группа по умолчанию.",
"cmd.error": "Ошибка команды: {{.Error}}"
"cmd.error": "Ошибка команды: {{.Error}}",
"firewall_rules_not_found": "Не найдены правила брандмауэра",
"firewall_rules_not_found_body": "Правила брандмауэра не найдены. Параметр «Восстановить правила» отключен. В данный момент вы незащищены!!!",
"firewall_rules_not_found_body_recover_error": "Правила брандмауэра не найдены. Восстановить правила брандмауэра не удалось. В данный момент вы незащищены!!! Ошибка при восстановлении правил брандмауэра: {{.Error}}",
"firewall_rules_not_found_body_recover_success": "Правила брандмауэра не были обнаружены. Однако правила брандмауэра были успешно восстановлены!",
"firewall_rules_not_found_body_check_error": "Ошибка при проверке правил брандмауэра: {{.Error}}"
}
@@ -8,9 +8,10 @@ import (
)
type BruteForceProtectionPattern struct {
Regexp string `mapstructure:"regexp"`
IP int `mapstructure:"ip"`
Values []PatternValue
Regexp string `mapstructure:"regexp"`
IP int `mapstructure:"ip"`
Values []PatternValue
Partition *PatternPartition `mapstructure:"partition"`
}
func (p *BruteForceProtectionPattern) ToPattern() (brute_force_protection.RegexPattern, error) {
@@ -32,6 +33,14 @@ func (p *BruteForceProtectionPattern) ToPattern() (brute_force_protection.RegexP
pattern.Values = append(pattern.Values, v)
}
if p.Partition != nil {
partition, err := p.Partition.ToPatternPartition()
if err != nil {
return brute_force_protection.RegexPattern{}, err
}
pattern.Partition = partition
}
return pattern, nil
}
+11 -2
View File
@@ -6,8 +6,9 @@ import (
)
type LogAlertPattern struct {
Regexp string `mapstructure:"regexp"`
Values []PatternValue
Regexp string `mapstructure:"regexp"`
Values []PatternValue
Partition *PatternPartition `mapstructure:"partition"`
}
func (p *LogAlertPattern) ToPattern() (config.AlertRegexPattern, error) {
@@ -24,5 +25,13 @@ func (p *LogAlertPattern) ToPattern() (config.AlertRegexPattern, error) {
pattern.Values = append(pattern.Values, v)
}
if p.Partition != nil {
partition, err := p.Partition.ToPatternPartition()
if err != nil {
return config.AlertRegexPattern{}, err
}
pattern.Partition = partition
}
return pattern, nil
}
@@ -0,0 +1,46 @@
package analyzer
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/partition"
)
type PatternPartition struct {
Value int `mapstructure:"value"`
Trim bool `mapstructure:"trim"`
LowerCase bool `mapstructure:"lower_case"`
Type *PatternPartitionType `mapstructure:"type"`
}
func (p *PatternPartition) ToPatternPartition() (*partition.PatternPartition, error) {
if err := p.validate(); err != nil {
return nil, err
}
normalize := partition.NewNormalize(p.Trim, p.LowerCase)
var patternPartitionType partition.PatternPartitionType
if p.Type != nil {
if partitionType, err := p.Type.ToType(normalize); err != nil {
return nil, err
} else {
patternPartitionType = partitionType
}
}
return &partition.PatternPartition{
Value: uint8(p.Value),
Type: patternPartitionType,
Normalize: normalize.Normalize,
}, nil
}
func (p *PatternPartition) validate() error {
if p.Value <= 0 || p.Value > 255 {
return errors.New("invalid partition value. min: 1, max: 255")
}
return nil
}
@@ -0,0 +1,42 @@
package analyzer
import (
"errors"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/partition"
)
type PatternPartitionType struct {
Type string `mapstructure:"type"`
Keywords []string `mapstructure:"keywords"`
Partitioned bool `mapstructure:"partitioned"`
}
func (p *PatternPartitionType) ToType(normalize partition.Normalize) (partition.PatternPartitionType, error) {
if err := p.validate(); err != nil {
return nil, err
}
var keywords []string
for _, keyword := range p.Keywords {
keywords = append(keywords, normalize.Normalize(keyword))
}
if p.Type == "except" {
return partition.NewExceptType(keywords, p.Partitioned), nil
}
return partition.NewOnlyType(keywords, p.Partitioned), nil
}
func (p *PatternPartitionType) validate() error {
if p.Type != "only" && p.Type != "except" {
return errors.New("invalid partition type. only 'only' and 'except' are supported")
}
if len(p.Keywords) == 0 {
return errors.New("invalid partition type. keywords are required")
}
return nil
}
+24 -3
View File
@@ -3,6 +3,7 @@ package blocklists
import (
"fmt"
"git.kor-elf.net/kor-elf-shield/blocklist/parser"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/blocklist"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
@@ -10,8 +11,9 @@ import (
)
type Setting struct {
Enabled bool `mapstructure:"enabled"`
Sources []Sources
Enabled bool `mapstructure:"enabled"`
ExcludeIPs []string `mapstructure:"exclude_ips"`
Sources []Sources
}
func InitSetting(path string) (Setting, error) {
@@ -42,6 +44,14 @@ func InitSetting(path string) (Setting, error) {
func settingDefault() Setting {
return Setting{
Enabled: false,
ExcludeIPs: []string{
"127.0.0.1/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fc00::/7",
},
Sources: []Sources{},
}
}
@@ -52,6 +62,17 @@ func (b *Setting) ToSources(logger log.Logger) []*blocklist.SourceConfig {
return sources
}
logger.Debug(fmt.Sprintf("exclude IPs: %v", b.ExcludeIPs))
var exclusionChecker parser.ExclusionChecker
if len(b.ExcludeIPs) > 0 {
if checker, err := parser.NewExclusionChecker(b.ExcludeIPs); err != nil {
logger.Warn(fmt.Sprintf("failed to create exclusion checker: %s", err))
} else {
exclusionChecker = checker
}
}
sourceNames := make(map[string]string)
for _, source := range b.Sources {
@@ -65,7 +86,7 @@ func (b *Setting) ToSources(logger log.Logger) []*blocklist.SourceConfig {
}
sourceNames[source.Name] = source.Name
sourceConfig, err := source.ToSourceConfig()
sourceConfig, err := source.ToSourceConfig(exclusionChecker)
if err != nil {
logger.Warn(fmt.Sprintf("failed to convert source: %s", err))
continue
+5 -3
View File
@@ -39,7 +39,7 @@ type Sources struct {
RssFieldSeparator string `mapstructure:"rss_field_separator"`
}
func (s *Sources) ToSourceConfig() (*daemonBlocklist.SourceConfig, error) {
func (s *Sources) ToSourceConfig(exclusionChecker parser.ExclusionChecker) (*daemonBlocklist.SourceConfig, error) {
if err := s.Validate(); err != nil {
return &daemonBlocklist.SourceConfig{}, err
}
@@ -49,9 +49,11 @@ func (s *Sources) ToSourceConfig() (*daemonBlocklist.SourceConfig, error) {
return &daemonBlocklist.SourceConfig{}, err
}
config := blocklist.NewConfig(uint(s.Limit))
config := blocklist.NewConfigWithExclusionChecker(uint(s.Limit), exclusionChecker)
if s.TxtType == "interval" {
config.Validator = &parser.IPRangeValidator{}
config.Validator = &parser.IPRangeValidator{
ExclusionChecker: exclusionChecker,
}
}
if s.Zip {
+5
View File
@@ -17,6 +17,7 @@ type Setting struct {
MetadataNaming metadataNaming
Policy policy
PortKnocking []portKnocking
RulesGuard RulesGuard
}
func InitSetting(path string) (Setting, error) {
@@ -53,6 +54,7 @@ func settingDefault() Setting {
MetadataNaming: defaultMetadataNaming(),
Policy: defaultPolicy(),
PortKnocking: defaultPortKnocking(),
RulesGuard: defaultRulesGuard(),
}
}
@@ -72,6 +74,9 @@ func (s Setting) Validate() error {
if err := s.Options.Validate(); err != nil {
return err
}
if err := s.RulesGuard.Validate(); err != nil {
return err
}
return nil
}
+40
View File
@@ -0,0 +1,40 @@
package firewall
import (
"fmt"
GuardConfig "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard/config"
)
type RulesGuard struct {
Enabled bool `mapstructure:"enabled"`
Notifications bool `mapstructure:"notifications"`
Recovery bool `mapstructure:"recovery"`
Interval int32 `mapstructure:"interval"`
}
func defaultRulesGuard() RulesGuard {
return RulesGuard{
Enabled: true,
Notifications: true,
Recovery: true,
Interval: 3600,
}
}
func (r *RulesGuard) Validate() error {
if r.Interval < 60 {
return fmt.Errorf("interval must be greater than 60")
}
return nil
}
func (r *RulesGuard) ToGuardConfig() GuardConfig.GuardConfig {
return GuardConfig.GuardConfig{
Enable: r.Enabled,
Notifications: r.Notifications,
Recovery: r.Recovery,
Interval: uint32(r.Interval),
}
}
+14 -9
View File
@@ -7,6 +7,7 @@ import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/blocklist"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/docker_monitor"
firewallConfig "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/config"
GuardConfig "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/guard/config"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
@@ -42,38 +43,38 @@ func otherSettingsPathDefault() *otherSettingsPath {
}
}
func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewallConfig.Config, error) {
func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewallConfig.Config, GuardConfig.GuardConfig, error) {
setting, err := firewallSetting.InitSetting(o.Firewall)
if err != nil {
return firewallConfig.Config{}, err
return firewallConfig.Config{}, GuardConfig.GuardConfig{}, err
}
configPolicy, err := setting.Policy.ToConfigPolicy()
if err != nil {
return firewallConfig.Config{}, err
return firewallConfig.Config{}, GuardConfig.GuardConfig{}, err
}
inPorts, outPorts, err := setting.ToPorts()
if err != nil {
return firewallConfig.Config{}, err
return firewallConfig.Config{}, GuardConfig.GuardConfig{}, err
}
IPs, err := setting.ToIPs()
if err != nil {
return firewallConfig.Config{}, err
return firewallConfig.Config{}, GuardConfig.GuardConfig{}, err
}
optionClearMode, err := setting.Options.ToClearMode()
if err != nil {
return firewallConfig.Config{}, err
return firewallConfig.Config{}, GuardConfig.GuardConfig{}, err
}
portKnocking, err := setting.ToConfigPortKnocking()
if err != nil {
return firewallConfig.Config{}, err
return firewallConfig.Config{}, GuardConfig.GuardConfig{}, err
}
return firewallConfig.Config{
firewall := firewallConfig.Config{
InPorts: inPorts,
OutPorts: outPorts,
IP4: firewallConfig.ConfigIP4{
@@ -109,7 +110,11 @@ func (o *otherSettingsPath) ToFirewallConfig(dockerSupport bool) (firewallConfig
},
Policy: configPolicy,
PortKnocking: portKnocking,
}, nil
}
rulesGuard := setting.RulesGuard.ToGuardConfig()
return firewall, rulesGuard, nil
}
func (o *otherSettingsPath) ToNotificationsConfig() (notifications.Config, error) {
+8 -7
View File
@@ -68,7 +68,7 @@ func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, erro
}))
}
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
firewallConfig, guardConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
if err != nil {
return daemon.DaemonOptions{}, err
}
@@ -79,12 +79,13 @@ func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, erro
}
return daemon.DaemonOptions{
PathPidFile: s.PidFile,
PathSocketFile: s.SocketFile,
DataDir: s.DataDir,
PathNftables: s.BinaryLocations.Nftables,
ConfigFirewall: firewallConfig,
ConfigAnalyzer: analyzerConfig,
PathPidFile: s.PidFile,
PathSocketFile: s.SocketFile,
DataDir: s.DataDir,
PathNftables: s.BinaryLocations.Nftables,
ConfigFirewall: firewallConfig,
ConfigFirewallGuard: guardConfig,
ConfigAnalyzer: analyzerConfig,
}, nil
}