Merge pull request 'v0.12.0' (#13) from develop into main
Reviewed-on: #13
This commit is contained in:
@@ -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)
|
||||
#### Русский
|
||||
* Добавлено: отображение номера уведомления в групповых уведомлениях.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
###
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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()),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user