Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3e44359d1 | ||
|
|
b782b7c975
|
||
|
|
e28fafc1e9
|
||
|
|
350c78c40a
|
||
|
|
22d7f901d0
|
||
|
|
1cff4fb2ba
|
||
|
|
2d219edd23
|
||
|
|
1cc52ca28e
|
||
|
|
7392e753ea
|
||
|
|
6148b0a06d
|
||
|
|
7322455ae0
|
||
|
|
232b63a207
|
||
|
|
4d4e7272ce
|
||
|
|
5c2b34b742
|
||
|
|
5315a4e0ff
|
||
|
|
0286ebc141
|
||
|
|
9759d9805b
|
||
|
|
29df5f3fd8
|
||
|
|
f2a9dc41be
|
||
|
|
f2dfeb507d
|
||
|
|
bae20cbf49 | ||
|
|
f9c438c973
|
||
|
|
cbf3dd6402
|
||
|
|
8495d7c4e2
|
||
|
|
311add0e8b
|
||
|
|
c26a898d05
|
||
|
|
9d5b6c7d6b
|
||
|
|
a0893ea361
|
||
|
|
0111304d85
|
||
|
|
aa0c5a9551
|
||
|
|
65806d701a
|
||
|
|
8c10ab0e62
|
||
|
|
95aec171e0
|
||
|
|
666f563ef5
|
||
|
|
ed7bd905f8
|
||
|
|
d06dd6e9fc
|
||
|
|
f650e89610
|
||
|
|
8c55ad1b23
|
||
|
|
17efaaa36a
|
||
|
|
df469a6f6d
|
||
|
|
6de78c2708
|
||
|
|
87add28edd
|
||
|
|
7cdd761dcd | ||
|
|
bdbbd303cc
|
||
|
|
b7cfc756c1
|
||
|
|
b396963976
|
||
|
|
eb4b0b4e4f
|
||
|
|
03a1d46f1b
|
||
|
|
eda1b956f3
|
||
|
|
14c4c504fe
|
||
|
|
5acc8a72de
|
||
|
|
1047c75629
|
||
|
|
f93716e194
|
+75
-10
@@ -1,4 +1,69 @@
|
||||
## 0.11.0 (7.5.2026)
|
||||
## 0.14.0 (05.09.2026)
|
||||
#### Русский
|
||||
* Исправлена ошибка в правилах исходящего ICMP: при `icmp_timestamp_drop=true` теперь корректно блокируются `timestamp-reply` пакеты (вместо `timestamp-request`).
|
||||
* Добавлена новая команда `kor-elf-shield config test`. Теперь перед перезапуском демона можно проверить корректность конфигурации.
|
||||
***
|
||||
#### English
|
||||
* Fixed an issue in outbound ICMP rules: when `icmp_timestamp_drop=true`, `timestamp-reply` packets are now correctly blocked (instead of `timestamp-request`).
|
||||
* Added a new command, `kor-elf-shield config test`. Now you can check the configuration before restarting the daemon.
|
||||
***
|
||||
## 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)
|
||||
#### Русский
|
||||
* Добавлено: отображение номера уведомления в групповых уведомлениях.
|
||||
* Добавлено: теперь в уведомлениях отображается количество блокировок IP-адреса.
|
||||
* Добавлены параметры для ограничения частоты уведомлений об успешной блокировке (`analyzer.toml`):
|
||||
* `ssh_notify_cooldown_seconds` — задаёт минимальный интервал в секундах между уведомлениями об успешных SSH-блокировках.
|
||||
* `ssh_notify_every` — отправляет следующее уведомление об успешной SSH-блокировке только после указанного количества новых сообщений о блокировке; имеет приоритет над `ssh_notify_cooldown_seconds`.
|
||||
* `notify_cooldown_seconds` — задаёт минимальный интервал в секундах между уведомлениями об успешных блокировках для пользовательских правил защиты от перебора пароля.
|
||||
* `notify_every` — отправляет следующее уведомление для пользовательского правила только после указанного количества новых сообщений о блокировке; имеет приоритет над `notify_cooldown_seconds`.
|
||||
***
|
||||
#### English
|
||||
* Added: display of notification number in group notifications.
|
||||
* Added: notifications now display how many times an IP address has been blocked.
|
||||
* Added options to limit the frequency of notifications about successful blocking (`analyzer.toml`):
|
||||
* `ssh_notify_cooldown_seconds` — Sets the minimum interval in seconds between notifications of successful SSH locks.
|
||||
* `ssh_notify_every` — Sends the next successful SSH lock notification only after the specified number of new lock messages; takes precedence over `ssh_notify_cooldown_seconds`.
|
||||
* `notify_cooldown_seconds` — Sets the minimum interval in seconds between notifications of successful locks for custom password attack protection rules.
|
||||
* `notify_every` — Sends the next notification for the custom rule only after the specified number of new block messages; takes precedence over `notify_cooldown_seconds`.
|
||||
***
|
||||
## 0.11.0 (07.05.2026)
|
||||
#### Русский
|
||||
* В настройки файла `firewall.toml` добавлен параметр `options.cache`.
|
||||
* Этот параметр включает кэширование, чтобы избежать постоянной компиляции команд nftables во временный файл. Файл кэша изменяется после изменения настроек или обновления версии программы. (`Включено по умолчанию`)
|
||||
@@ -25,7 +90,7 @@
|
||||
* Improved `Uptime` output in `kor-elf-shield status` command.
|
||||
* Improved display of blocking time in notifications.
|
||||
***
|
||||
## 0.10.0 (12.4.2026)
|
||||
## 0.10.0 (12.04.2026)
|
||||
#### Русский
|
||||
* При автоматической блокировке добавил возможность получать данные об IP-адресах (континент, страна, город, часовой пояс).
|
||||
* В файл analyzer.toml добавлен параметр type к [[logAlert.rules.patterns.values]] в котором можно указать тип "ip". Это позволит для этого поля получить данные об IP-адресе при отправке оповещения.
|
||||
@@ -46,7 +111,7 @@
|
||||
* The otherSettingsPath.geoip parameter has been added to the kor-elf-shield.toml file.
|
||||
* A new geoip.toml settings file has been added. This file configures parameters for retrieving IP address data.
|
||||
***
|
||||
## 0.9.0 (21.3.2026)
|
||||
## 0.9.0 (21.03.2026)
|
||||
#### Русский
|
||||
* Добавилась поддержка Port knocking.
|
||||
* В firewall.toml добавился раздел Port knocking.
|
||||
@@ -78,7 +143,7 @@
|
||||
* GreenSnow Hack List
|
||||
* Added the otherSettingsPath.blocklists parameter to the kor-elf-shield.toml settings.
|
||||
* Added a new settings file, blocklists.toml. It contains settings for obtaining a list of IP addresses to block.
|
||||
## 0.8.0 (9.3.2026)
|
||||
## 0.8.0 (09.03.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Теперь можно тонко настроить блокировку портов для IP адреса, который пытается подобрать пароль.
|
||||
@@ -102,7 +167,7 @@
|
||||
* The `kor-elf-shield block add` command has been added. This command can be used to block an IP address. See `kor-elf-shield block add --help` for details.
|
||||
* The `kor-elf-shield block delete` command has been added. This command can be used to delete a blocked IP address. See `kor-elf-shield block delete --help` for details.
|
||||
***
|
||||
## 0.7.0 (28.2.2026)
|
||||
## 0.7.0 (28.02.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена возможность настройки отслеживания событий в журналах.
|
||||
@@ -116,7 +181,7 @@
|
||||
* New parameters have been added to the analyzer.toml settings file. See the full list at: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/187c447301b9c0bfa41ec2b2c9435ab0ce44bed6/assets/configs/analyzer.toml
|
||||
* Added the `kor-elf-shield ban clear` command, which unbans all banned IP addresses.
|
||||
***
|
||||
## 0.6.0 (8.2.2026)
|
||||
## 0.6.0 (08.02.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена возможность повторной отправки уведомления, если в прошлый раз произошла ошибка.
|
||||
@@ -138,7 +203,7 @@
|
||||
* enable_retries = Enables repeated attempts to send a notification if the first attempt fails. Default: true
|
||||
* retry_interval = The time interval in seconds between attempts. Default: 600
|
||||
***
|
||||
## 0.5.0 (17.1.2026)
|
||||
## 0.5.0 (17.01.2026)
|
||||
***
|
||||
#### Русский
|
||||
* В настройках analyzer.toml добавил параметры local_enable и local_notify.
|
||||
@@ -162,7 +227,7 @@
|
||||
* sudo_enable = Enables tracking of logins via sudo. Off by default.
|
||||
* sudo_notify = Enables notifications about logins via sudo. Enabled by default.
|
||||
***
|
||||
## 0.4.0 (11.1.2026)
|
||||
## 0.4.0 (11.01.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Удалён параметр options.docker_support из файла firewall.toml. Настройки от Docker перенесены в файл docker.toml.
|
||||
@@ -184,7 +249,7 @@
|
||||
* The program crashed after Docker was stopped.
|
||||
* The IP addresses specified in the settings were not blocked during redirection to the Docker container.
|
||||
***
|
||||
## 0.3.0 (4.1.2026)
|
||||
## 0.3.0 (04.01.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена частичная поддержка Docker.
|
||||
@@ -225,7 +290,7 @@
|
||||
* Added the output_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain output.
|
||||
* Added the forward_priority parameter to firewall.toml. You can specify a priority from -50 to 50 for chain forward.
|
||||
***
|
||||
## 0.1.0 (8.11.2025)
|
||||
## 0.1.0 (08.11.2025)
|
||||
***
|
||||
#### Русский
|
||||
* Реализована возможность настраивать nftables:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Kor-Elf Shield
|
||||
### <strong>Русский</strong> <a href="README_ENG.md">English</a>
|
||||
<p style="color: red; font-weight: bold">ВНИМАНИЕ: Программа на стадии активной разработки и на данный момент NOT PRODUCTION READY</p>
|
||||
<p>Я больше 10-ти лет использовал ConfigServer Security and Firewall (csf) для защиты своего сервера. Но, к сожалению, в сентябре узнал, что компания, которая поддерживала этот великолепный продукт, закрылась 31 августа 2025 года. CSF написан на языке PERL. И компания выложила все исходные коды в свой репозиторий под лицензией GPLv3. Но я не знаю язык PERL. И мне его тяжело читать. :)</p>
|
||||
<p>Я решил реализовать своё решение на языке Go Lang. Это не будет полная копия CSF. Просто CSF вдохновило меня сделать, что-то похожее для защиты своего сервера.</p>
|
||||
|
||||
@@ -62,6 +61,16 @@
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/firewall.toml</strong> - тут находятся настройки, связанные с nftables. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/firewall.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/firewall.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/analyzer.toml</strong> - тут находятся настройки для анализатора логов. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/analyzer.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/analyzer.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/notifications.toml</strong> - тут находятся настройки для уведомлений. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/notifications.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/notifications.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/docker.toml</strong> - тут находятся настройки, связанные с docker. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/docker.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/docker.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/blocklists.toml</strong> - тут находятся настройки для получения IP-адресов от разных сервисов, которые надо заблокировать. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/blocklists.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/blocklists.toml</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/geoip.toml</strong> - тут находятся настройки для получения данных об IP-адресах. Информацию можно посмотреть тут: <a href="https://shield.kor-elf.net/docs/0.x/geoip.toml" target="_blank">https://shield.kor-elf.net/docs/0.x/geoip.toml</a></p>
|
||||
|
||||
***
|
||||
|
||||
<p>Программное обеспечение является MIT (см. <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE">LICENSE</a>) и использует сторонние библиотеки, которые распространяются на их собственных условиях (см. <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE-3RD-PARTY.txt">LICENSE-3RD-PARTY.txt</a>).</p>
|
||||
+10
-1
@@ -1,6 +1,5 @@
|
||||
# Kor-Elf Shield
|
||||
### <strong>English</strong> <a href="README.md">Русский</a>
|
||||
<p style="color: red; font-weight: bold">ATTENTION: The program is under active development and is NOT CURRENTLY PRODUCTION READY</p>
|
||||
<p>I have been using ConfigServer Security and Firewall (csf) for over 10 years to protect my server. But unfortunately, in September I found out that the company that supported this great product closed on August 31, 2025. CSF is written in PERL. And the company uploaded all the source codes to its repository under the GPLv3 license. But I do not know the PERL language. And it's hard for me to read it. :)</p>
|
||||
<p>I decided to implement my solution in the Go Lang language. It will not be a complete copy of CSF. CSF just inspired me to do something similar to protect my server.</p>
|
||||
|
||||
@@ -62,6 +61,16 @@
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/firewall.toml</strong> - Here are the settings related to nftables. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/firewall.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/firewall.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/analyzer.toml</strong> - Contains log analyzer settings. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/analyzer.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/analyzer.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/notifications.toml</strong> - Contains notification settings. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/notifications.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/notifications.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/docker.toml</strong> - Contains Docker-related settings. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/docker.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/docker.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/blocklists.toml</strong> - Contains settings for obtaining IP addresses from various services that need to be blocked. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/blocklists.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/blocklists.toml/language/en</a></p>
|
||||
|
||||
<p><strong>/etc/kor-elf-shield/geoip.toml</strong> - This contains settings for obtaining IP address data. Information can be found here: <a href="https://shield.kor-elf.net/docs/0.x/geoip.toml/language/en" target="_blank">https://shield.kor-elf.net/docs/0.x/geoip.toml/language/en</a></p>
|
||||
|
||||
***
|
||||
|
||||
<p>The software is MIT (see <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE">LICENSE</a>) and uses third-party libraries that are distributed on their own terms (see <a href="https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/branch/main/LICENSE-3RD-PARTY.txt">LICENSE-3RD-PARTY.txt</a>).</p>
|
||||
@@ -56,7 +56,7 @@ blocking_time = 3600
|
||||
rate_limit_period = 3600
|
||||
|
||||
###
|
||||
# Указываем в секундах, через какое время сбрасывать данные IP в групе _default если не было событий.
|
||||
# Указываем в секундах, через какое время сбрасывать данные IP в группе _default если не было событий.
|
||||
# Если указать 0, то не будет сбрасывать.
|
||||
# По умолчанию: 86400
|
||||
# ***
|
||||
@@ -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
|
||||
@@ -86,6 +92,34 @@ ssh_enable = true
|
||||
###
|
||||
ssh_notify = true
|
||||
|
||||
###
|
||||
# Минимальный интервал в секундах между уведомлениями об успешной SSH-блокировке.
|
||||
# После отправки уведомления повторные уведомления не будут отправляться в течение указанного времени.
|
||||
# Если указать 0, ограничение по времени отключено.
|
||||
# По умолчанию: 0
|
||||
# ***
|
||||
# Minimum interval in seconds between notifications about successful SSH blocks.
|
||||
# After a notification is sent, repeated notifications will not be sent for the specified time.
|
||||
# If you specify 0, the time limit is disabled.
|
||||
# Default: 0
|
||||
###
|
||||
ssh_notify_cooldown_seconds = 0
|
||||
|
||||
###
|
||||
# Количество успешных SSH-блокировок, которое нужно пропустить после отправки уведомления.
|
||||
# Следующее уведомление будет отправлено после указанного количества новых сообщений о блокировке.
|
||||
# Этот параметр имеет приоритет над ssh_notify_cooldown_seconds.
|
||||
# Если указать 0, ограничение по количеству сообщений отключено.
|
||||
# По умолчанию: 0
|
||||
# ***
|
||||
# Number of successful SSH block messages to skip after sending a notification.
|
||||
# The next notification will be sent after the specified number of new block messages.
|
||||
# This parameter has priority over ssh_notify_cooldown_seconds.
|
||||
# If you specify 0, the message-count limit is disabled.
|
||||
# Default: 0
|
||||
###
|
||||
ssh_notify_every = 0
|
||||
|
||||
###
|
||||
# Можно указать свою группу, чтобы связать с другими правилами.
|
||||
# По умолчанию: ""
|
||||
@@ -101,7 +135,7 @@ ssh_group = ""
|
||||
# [[bruteForceProtection.groups]]
|
||||
# name = "my_name_group" # Имя группы. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Любой текст группы" # Текст уведомления (обязательное поле)
|
||||
# rate_limit_reset_period = 86400 # Указываем в секундах, через какое время сбрасывать данные в групе если не было событий. Если указать 0, то не будет сбрасывать.
|
||||
# rate_limit_reset_period = 86400 # Указываем в секундах, через какое время сбрасывать данные в групе, если не было событий. Если указать 0, то не будет сбрасывать.
|
||||
## block_type = "ip_port" # Указываем тип блокировки: ip, ip_port. Если ничего не укажите, будет указан тип ip.
|
||||
## ports = ["22/tcp", "22/udp"] # Если тип блокировки стоит ip_port, то нужно указать порты, которые будут заблокированы после обнаружения попытки перебора пароля.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
@@ -162,11 +196,14 @@ ssh_group = ""
|
||||
# [[bruteForceProtection.rules]]
|
||||
# enabled = true # Включает или выключает отслеживания (обязательное поле)
|
||||
# notify = true # Включает или выключает уведомления (обязательное поле)
|
||||
# notify_cooldown_seconds = 0 # Минимальный интервал в секундах между уведомлениями об успешной блокировке (не обязательное поле)
|
||||
# notify_every = 0 # Количество успешных блокировок, которое нужно пропустить после отправки уведомления (не обязательное поле)
|
||||
# name = "my_name_rule" # Имя уведомления. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Ваш любой текст для уведомления" # Текст уведомления (обязательное поле)
|
||||
# group = "my_name_group" # Можно указать имя группы (не обязательное поле)
|
||||
# [bruteForceProtection.rules.source]
|
||||
# type = "journalctl" # journalctl или file (обязательное поле)
|
||||
# path = "" # Указываем путь к файлу (обязательное поле если type = "file")
|
||||
# field = "systemd_unit" # systemd_unit или syslog_identifier (обязательное поле если type = "journalctl")
|
||||
# match = "ssh.service" # Значение (обязательное поле если type = "journalctl")
|
||||
# если field = "systemd_unit", то match должен заканичваться: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
@@ -176,18 +213,28 @@ 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:
|
||||
# [[bruteForceProtection.rules]]
|
||||
# enabled = true # Enables or disables tracking (required)
|
||||
# notify = true # Enables or disables notifications (required)
|
||||
# notify_cooldown_seconds = 0 # Minimum interval in seconds between notifications about successful blocks (optional field)
|
||||
# notify_every = 0 # Number of successful block messages to skip after sending a notification (optional field)
|
||||
# name = "my_name_rule" # Notification name. Allowed characters are "a-z, A-Z, -, _". The first character must be a letter (required field)
|
||||
# message = "Your any text for notification" # Notification text (required field)
|
||||
# group = "my_name_group" # You can specify the group name (optional field)
|
||||
# [bruteForceProtection.rules.source]
|
||||
# type = "journalctl" # journalctl or file (required)
|
||||
# path = "" # Specify the path to the file (required field if type = "file")
|
||||
# field = "systemd_unit" # systemd_unit or syslog_identifier (required if type = "journalctl")
|
||||
# match = "ssh.service" # Value (required if type = "journalctl")
|
||||
# If field = "systemd_unit", then match must end with: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
@@ -199,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
|
||||
###
|
||||
|
||||
###############################################################################
|
||||
@@ -391,6 +446,7 @@ notify = true
|
||||
# group = "my_name_group" # Можно указать имя группы (не обязательное поле)
|
||||
# [logAlert.rules.source]
|
||||
# type = "journalctl" # journalctl или file (обязательное поле)
|
||||
# path = "" # Указываем путь к файлу (обязательное поле если type = "file")
|
||||
# field = "systemd_unit" # systemd_unit или syslog_identifier (обязательное поле если type = "journalctl")
|
||||
# match = "ssh.service" # Значение (обязательное поле если type = "journalctl")
|
||||
# если field = "systemd_unit", то match должен заканичваться: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
@@ -403,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.
|
||||
@@ -415,6 +479,7 @@ notify = true
|
||||
# group = "my_name_group" # You can specify the group name (optional field)
|
||||
# [logAlert.rules.source]
|
||||
# type = "journalctl" # journalctl or file (required)
|
||||
# path = "" # Specify the path to the file (required field if type = "file")
|
||||
# field = "systemd_unit" # systemd_unit or syslog_identifier (required if type = "journalctl")
|
||||
# match = "ssh.service" # Value (required if type = "journalctl")
|
||||
# If field = "systemd_unit", then match must end with: .service, .socket, .target, .mount, .timer, .path, .scope, .slice, .device
|
||||
@@ -427,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
|
||||
|
||||
|
||||
@@ -165,12 +165,12 @@ encoding = "json"
|
||||
|
||||
###
|
||||
# Список мест для основных логов.
|
||||
# Можно указать несколько путей. Например output_paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Можно указать несколько путей. Например paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Где "stdout" - вывод данных в консоль.
|
||||
# По умолчанию: ["/var/log/kor-elf-shield.log"]
|
||||
# ***
|
||||
# List of locations for main logs.
|
||||
# Multiple paths can be specified. For example, output_paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Multiple paths can be specified. For example, paths = ["stdout", "/var/log/kor-elf-shield.log"].
|
||||
# Where "stdout" is the console output.
|
||||
# Default: ["/var/log/kor-elf-shield.log"]
|
||||
###
|
||||
@@ -178,12 +178,12 @@ paths = ["/var/log/kor-elf-shield.log"]
|
||||
|
||||
###
|
||||
# Список мест для ошибок логгирования. Например журнал не смог записать какие-то данные.
|
||||
# Можно указать несколько путей. Например output_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Можно указать несколько путей. Например log_error_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Где "stderr" - вывод данных в консоль.
|
||||
# По умолчанию: ["stderr"]
|
||||
# ***
|
||||
# List of locations for logging errors. For example, the log failed to write some data.
|
||||
# Multiple paths can be specified. For example, output_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Multiple paths can be specified. For example, log_error_paths = ["stderr", "/var/log/kor-elf-shield-log-error.log"].
|
||||
# Where "stderr" is the data output to the console.
|
||||
# Default: ["stderr"]
|
||||
###
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CmdConfig() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "config",
|
||||
Usage: i18n.Lang.T("cmd.daemon.config.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "test",
|
||||
Usage: i18n.Lang.T("cmd.daemon.config.test.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.config.test.Description"),
|
||||
Action: CmdTestConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CmdTestConfig(_ context.Context, _ *cli.Command) error {
|
||||
falseLogger := log.NewFalseLogger()
|
||||
|
||||
testMain := testMainConfig()
|
||||
testDocker, dockerSupport := testDockerConfig()
|
||||
testFirewall := testFirewallConfig(dockerSupport)
|
||||
testAnalyzer := testAnalyzerConfig()
|
||||
testNotifications := testNotificationsConfig()
|
||||
testBlocklists := testBlocklistsConfig(falseLogger)
|
||||
testGeoip := testGeoipConfig(falseLogger)
|
||||
|
||||
checkNft := checkProgramNFT()
|
||||
checkJournalctl := checkProgramJournalctl()
|
||||
checkDocker := ""
|
||||
if dockerSupport {
|
||||
checkDocker = "\n " + checkProgramDocker()
|
||||
}
|
||||
|
||||
fmt.Println(
|
||||
"***\n"+i18n.Lang.T("cmd.daemon.config.test.settingTitle"),
|
||||
"\n", testMain,
|
||||
"\n", testFirewall,
|
||||
"\n", testAnalyzer,
|
||||
"\n", testNotifications,
|
||||
"\n", testDocker,
|
||||
"\n", testBlocklists,
|
||||
"\n", testGeoip,
|
||||
"\n"+i18n.Lang.T("cmd.daemon.config.test.checkingPrograms"),
|
||||
"\n", checkNft,
|
||||
"\n", checkJournalctl,
|
||||
checkDocker,
|
||||
"\n***",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func testMainConfig() string {
|
||||
configTitle := i18n.Lang.T("cmd.daemon.config.test.main")
|
||||
if err := setting.Config.Validate(); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
if err := setting.Config.ValidateBeforeStart(); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testDockerConfig() (message string, dockerSupport bool) {
|
||||
configTitle := "docker"
|
||||
_, dockerSupport, err := setting.Config.OtherSettingsPath.ToDockerConfig(setting.Config.BinaryLocations)
|
||||
if err != nil {
|
||||
return resultError(configTitle, err), false
|
||||
}
|
||||
|
||||
return resultOk(configTitle), dockerSupport
|
||||
}
|
||||
|
||||
func testFirewallConfig(dockerSupport bool) string {
|
||||
configTitle := "firewall"
|
||||
if _, _, err := setting.Config.OtherSettingsPath.ToFirewallConfig(dockerSupport); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testAnalyzerConfig() string {
|
||||
configTitle := "analyzer"
|
||||
if _, err := setting.Config.OtherSettingsPath.ToAnalyzerConfig(setting.Config.BinaryLocations); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testNotificationsConfig() string {
|
||||
configTitle := "notifications"
|
||||
if _, err := setting.Config.OtherSettingsPath.ToNotificationsConfig(); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testBlocklistsConfig(logger log.Logger) string {
|
||||
configTitle := "blocklists"
|
||||
if _, _, err := setting.Config.OtherSettingsPath.ToBlocklistConfig(logger); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func testGeoipConfig(logger log.Logger) string {
|
||||
configTitle := "geoip"
|
||||
if _, _, err := setting.Config.OtherSettingsPath.ToConfig(setting.Config.DataDir, logger); err != nil {
|
||||
return resultError(configTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(configTitle)
|
||||
}
|
||||
|
||||
func checkProgramNFT() string {
|
||||
programTitle := "nftables"
|
||||
path := setting.Config.BinaryLocations.Nftables
|
||||
if path == "" {
|
||||
return resultError(programTitle, errors.New(i18n.Lang.T("cmd.daemon.config.test.pathEmpty", map[string]interface{}{"Program": programTitle})))
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return resultError(programTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(programTitle)
|
||||
}
|
||||
|
||||
func checkProgramJournalctl() string {
|
||||
programTitle := "journalctl"
|
||||
path := setting.Config.BinaryLocations.Journalctl
|
||||
if path == "" {
|
||||
return resultError(programTitle, errors.New(i18n.Lang.T("cmd.daemon.config.test.pathEmpty", map[string]interface{}{"Program": programTitle})))
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return resultError(programTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(programTitle)
|
||||
}
|
||||
|
||||
func checkProgramDocker() string {
|
||||
programTitle := "docker"
|
||||
path := setting.Config.BinaryLocations.Docker
|
||||
if path == "" {
|
||||
return resultError(programTitle, errors.New(i18n.Lang.T("cmd.daemon.config.test.pathEmpty", map[string]interface{}{"Program": programTitle})))
|
||||
}
|
||||
|
||||
cmd := exec.Command(path, "--version")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return resultError(programTitle, err)
|
||||
}
|
||||
|
||||
return resultOk(programTitle)
|
||||
}
|
||||
|
||||
func resultOk(title string) string {
|
||||
return fmt.Sprintf("%s: \033[32mOk\033[0m", title)
|
||||
}
|
||||
|
||||
func resultError(title string, err error) string {
|
||||
errText := i18n.Lang.T("cmd.daemon.config.test.error", map[string]interface{}{"Error": err})
|
||||
return fmt.Sprintf("%s: \033[31mError\n %s\u001B[0m", title, errText)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ func NewMainApp(appVer AppVersion, defaultConfigPath string) *cli.Command {
|
||||
daemon.CmdStart(),
|
||||
daemon.CmdStop(),
|
||||
daemon.CmdStatus(),
|
||||
daemon.CmdConfig(),
|
||||
daemon.CmdReopenLogger(),
|
||||
daemon.CmdNotifications(),
|
||||
daemon.CmdBlock(),
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
|
||||
)
|
||||
|
||||
func NewBruteForceProtectionSSH(isNotify bool, group *brute_force_protection.Group) ([]*Source, error) {
|
||||
func NewBruteForceProtectionSSH(isNotify bool, notifyCooldown int, notifyEvery int, group *brute_force_protection.Group) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSystemdUnit, "ssh.service")
|
||||
@@ -20,12 +20,26 @@ func NewBruteForceProtectionSSH(isNotify bool, group *brute_force_protection.Gro
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
BruteForceProtectionRule: &brute_force_protection.Rule{
|
||||
Name: "_ssh",
|
||||
Message: i18n.Lang.T("alert.bruteForceProtection.ssh.message"),
|
||||
IsNotification: isNotify,
|
||||
Name: "_ssh",
|
||||
Message: i18n.Lang.T("alert.bruteForceProtection.ssh.message"),
|
||||
|
||||
IsNotification: isNotify,
|
||||
NotificationCooldown: uint32(notifyCooldown),
|
||||
NotificationEvery: uint32(notifyEvery),
|
||||
|
||||
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,19 +1,27 @@
|
||||
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
|
||||
Message string
|
||||
IsNotification bool
|
||||
Patterns []RegexPattern
|
||||
Group *Group
|
||||
Name string
|
||||
Message string
|
||||
|
||||
IsNotification bool
|
||||
NotificationCooldown uint32
|
||||
NotificationEvery uint32
|
||||
|
||||
Patterns []RegexPattern
|
||||
Group *Group
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
analysisServices "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/alert_group"
|
||||
analysisBruteForceProtection "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection_group"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/geoip"
|
||||
@@ -33,6 +34,7 @@ func NewAnalysis(
|
||||
) Analysis {
|
||||
alertGroupService := alert_group.NewGroup(repositories.AlertGroup(), logger)
|
||||
bruteForceProtectionGroupService := brute_force_protection_group.NewGroup(repositories.BruteForceProtectionGroup(), logger)
|
||||
notificationPolicy := analysisBruteForceProtection.NewNotificationPolicy(repositories.BruteForceProtectionNotifyPolicy(), logger)
|
||||
|
||||
return &analysis{
|
||||
alertService: analysisServices.NewAlert(rulesIndex, alertGroupService, logger, notify, ipInfo),
|
||||
@@ -40,6 +42,7 @@ func NewAnalysis(
|
||||
rulesIndex,
|
||||
bruteForceProtectionGroupService,
|
||||
blockService,
|
||||
notificationPolicy,
|
||||
logger,
|
||||
notify,
|
||||
ipInfo,
|
||||
|
||||
@@ -26,15 +26,17 @@ type alert struct {
|
||||
}
|
||||
|
||||
type alertAnalyzeRuleReturn struct {
|
||||
found bool
|
||||
fields []*regexField
|
||||
found bool
|
||||
fields []*regexField
|
||||
partition *string
|
||||
}
|
||||
|
||||
type alertNotify struct {
|
||||
rule *config.AlertRule
|
||||
messages []string
|
||||
time time.Time
|
||||
fields []*regexField
|
||||
rule *config.AlertRule
|
||||
messages []string
|
||||
alertNumber uint64
|
||||
time time.Time
|
||||
fields []*regexField
|
||||
}
|
||||
|
||||
func NewAlert(
|
||||
@@ -64,9 +66,10 @@ func (a *alert) Analyze(entry *Entry) {
|
||||
continue
|
||||
}
|
||||
groupName := ""
|
||||
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
|
||||
@@ -79,15 +82,17 @@ func (a *alert) Analyze(entry *Entry) {
|
||||
for _, lastLog := range alertGroup.LastLogs {
|
||||
messages = append(messages, lastLog)
|
||||
}
|
||||
alertNumber = alertGroup.AlertNumber
|
||||
} else {
|
||||
messages = append(messages, entry.Message)
|
||||
}
|
||||
a.logger.Info(fmt.Sprintf("Alert detected (%s) (group:%s): %s", rule.Name, groupName, entry.Message))
|
||||
a.sendNotify(&alertNotify{
|
||||
rule: rule,
|
||||
messages: messages,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
rule: rule,
|
||||
messages: messages,
|
||||
alertNumber: alertNumber,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -98,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 {
|
||||
@@ -112,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 {
|
||||
@@ -122,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
|
||||
}
|
||||
|
||||
@@ -153,6 +183,7 @@ func (a *alert) sendNotify(notify *alertNotify) {
|
||||
text += i18n.Lang.T("time", map[string]any{
|
||||
"Time": notify.time,
|
||||
}) + "\n"
|
||||
|
||||
for _, field := range notify.fields {
|
||||
v := field.value
|
||||
if field.typeValue == config.PatternValueIP {
|
||||
@@ -164,6 +195,11 @@ func (a *alert) sendNotify(notify *alertNotify) {
|
||||
}
|
||||
text += fmt.Sprintf("%s: %s\n", field.name, v)
|
||||
}
|
||||
if notify.alertNumber > 0 {
|
||||
text += i18n.Lang.T("alertNumber", map[string]any{
|
||||
"Count": notify.alertNumber,
|
||||
}) + "\n"
|
||||
}
|
||||
text += "\n" + i18n.Lang.T("log", map[string]any{
|
||||
"Count": len(notify.messages),
|
||||
}) + "\n"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ type group struct {
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Alerted bool
|
||||
LastLogs []string
|
||||
Alerted bool
|
||||
AlertNumber uint64
|
||||
LastLogs []string
|
||||
}
|
||||
|
||||
func NewGroup(alertGroupRepository repository.AlertGroupRepository, logger log.Logger) Group {
|
||||
@@ -33,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
|
||||
@@ -101,6 +102,8 @@ func (g *group) analysisResult(rateLimit config.RateLimit, eventTime time.Time,
|
||||
entityAlertGroup.CurrentLevelTriggerCount++
|
||||
entityAlertGroup.TriggerCount = 0
|
||||
entityAlertGroup.LastLogs = []string{}
|
||||
|
||||
analysisResult.AlertNumber = entityAlertGroup.CurrentLevelTriggerCount
|
||||
} else {
|
||||
g.logger.Debug(fmt.Sprintf("Alert not reached rate limit"))
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
analysisBruteForceProtection "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection_group"
|
||||
"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/types"
|
||||
@@ -23,46 +24,51 @@ type BruteForceProtection interface {
|
||||
}
|
||||
|
||||
type bruteForceProtection struct {
|
||||
rulesIndex *RulesIndex
|
||||
groupService brute_force_protection_group.Group
|
||||
blockService brute_force_protection_group.BlockService
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
ipInfo geoip.Info
|
||||
rulesIndex *RulesIndex
|
||||
groupService brute_force_protection_group.Group
|
||||
blockService brute_force_protection_group.BlockService
|
||||
notificationPolicy analysisBruteForceProtection.NotificationPolicy
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
ipInfo geoip.Info
|
||||
}
|
||||
|
||||
type bruteForceProtectionAnalyzeRuleReturn struct {
|
||||
found bool
|
||||
fields []*regexField
|
||||
ip net.IP
|
||||
found bool
|
||||
fields []*regexField
|
||||
ip net.IP
|
||||
partition *string
|
||||
}
|
||||
|
||||
type bruteForceProtectionNotify struct {
|
||||
rule *brute_force_protection.Rule
|
||||
messages []string
|
||||
ip net.IP
|
||||
ports []types.L4Port
|
||||
time time.Time
|
||||
fields []*regexField
|
||||
blockSec uint32
|
||||
err error
|
||||
rule *brute_force_protection.Rule
|
||||
messages []string
|
||||
blockIPCount uint64
|
||||
ip net.IP
|
||||
ports []types.L4Port
|
||||
time time.Time
|
||||
fields []*regexField
|
||||
blockSec uint32
|
||||
err error
|
||||
}
|
||||
|
||||
func NewBruteForceProtection(
|
||||
rulesIndex *RulesIndex,
|
||||
groupService brute_force_protection_group.Group,
|
||||
blockService brute_force_protection_group.BlockService,
|
||||
notificationPolicy analysisBruteForceProtection.NotificationPolicy,
|
||||
logger log.Logger,
|
||||
notify notifications.Notifications,
|
||||
ipInfo geoip.Info,
|
||||
) BruteForceProtection {
|
||||
return &bruteForceProtection{
|
||||
rulesIndex: rulesIndex,
|
||||
groupService: groupService,
|
||||
blockService: blockService,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
ipInfo: ipInfo,
|
||||
rulesIndex: rulesIndex,
|
||||
groupService: groupService,
|
||||
blockService: blockService,
|
||||
notificationPolicy: notificationPolicy,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
ipInfo: ipInfo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,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
|
||||
@@ -122,26 +128,28 @@ func (p *bruteForceProtection) handleBlockIP(
|
||||
if isBanned == false {
|
||||
p.logger.Info(fmt.Sprintf("IP %s are not blocked (%s) (group:%s): %s. Err: %s", result.ip, rule.Name, rule.Group.Name, entry.Message, err.Error()))
|
||||
p.sendNotifyError(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
messages: groupResult.LastLogs,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.logger.Info(fmt.Sprintf("Block IP %s detected (%s) (group:%s): %s", result.ip, rule.Name, rule.Group.Name, entry.Message))
|
||||
p.sendNotifySuccess(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
messages: groupResult.LastLogs,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,36 +170,39 @@ func (p *bruteForceProtection) handleBlockIPWithPorts(
|
||||
if isBanned == false {
|
||||
p.logger.Info(fmt.Sprintf("IP %s are not blocked (%s) (group:%s): %s. Err: %s", result.ip, rule.Name, rule.Group.Name, entry.Message, err.Error()))
|
||||
p.sendNotifyError(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
ports: l4Ports,
|
||||
messages: groupResult.LastLogs,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
ports: l4Ports,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
p.logger.Info(fmt.Sprintf("Block IP %s detected (%s) (group:%s): %s", result.ip, rule.Name, rule.Group.Name, entry.Message))
|
||||
p.sendNotifySuccess(&bruteForceProtectionNotify{
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
ports: l4Ports,
|
||||
messages: groupResult.LastLogs,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
rule: rule,
|
||||
ip: result.ip,
|
||||
ports: l4Ports,
|
||||
messages: groupResult.LastLogs,
|
||||
blockIPCount: groupResult.BlockIPCount,
|
||||
time: entry.Time,
|
||||
fields: result.fields,
|
||||
blockSec: groupResult.BlockSec,
|
||||
err: err,
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -207,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,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
|
||||
}
|
||||
|
||||
@@ -240,7 +276,7 @@ func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, me
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotifySuccess(notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
if !p.notificationPolicy.IsNotify(notify.rule) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -310,6 +346,9 @@ func (p *bruteForceProtection) sendNotify(subject string, notify *bruteForceProt
|
||||
for _, field := range notify.fields {
|
||||
text += fmt.Sprintf("%s: %s\n", field.name, field.value)
|
||||
}
|
||||
text += i18n.Lang.T("blockIPCount", map[string]any{
|
||||
"Count": notify.blockIPCount,
|
||||
}) + "\n"
|
||||
text += "\n" + i18n.Lang.T("log", map[string]any{
|
||||
"Count": len(notify.messages),
|
||||
}) + "\n"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package brute_force_protection
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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/db/entity"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/repository"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type NotificationPolicy interface {
|
||||
IsNotify(rule *brute_force_protection.Rule) bool
|
||||
}
|
||||
|
||||
type notificationPolicy struct {
|
||||
notifyPolicyRepository repository.BruteForceProtectionNotifyPolicyRepository
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewNotificationPolicy(notifyPolicyRepository repository.BruteForceProtectionNotifyPolicyRepository, logger log.Logger) NotificationPolicy {
|
||||
return ¬ificationPolicy{
|
||||
notifyPolicyRepository: notifyPolicyRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *notificationPolicy) IsNotify(rule *brute_force_protection.Rule) bool {
|
||||
if !rule.IsNotification {
|
||||
return false
|
||||
}
|
||||
|
||||
if rule.NotificationCooldown == 0 && rule.NotificationEvery == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
isNotify := false
|
||||
|
||||
err := s.notifyPolicyRepository.Update(rule.Name, func(notify *entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error) {
|
||||
if isEvery(rule, notify) {
|
||||
isNotify = true
|
||||
return resetNotifyPolicy(notify), nil
|
||||
}
|
||||
notify.Every++
|
||||
|
||||
if rule.NotificationCooldown > 0 {
|
||||
lastNotifyAt := time.Unix(notify.CooldownTime, 0)
|
||||
cooldown := time.Duration(rule.NotificationCooldown) * time.Second
|
||||
if time.Since(lastNotifyAt) >= cooldown {
|
||||
isNotify = true
|
||||
return resetNotifyPolicy(notify), nil
|
||||
}
|
||||
}
|
||||
|
||||
return notify, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error(err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
return isNotify
|
||||
}
|
||||
|
||||
func isEvery(rule *brute_force_protection.Rule, entity *entity.BruteForceProtectionNotifyPolicy) bool {
|
||||
if rule.NotificationEvery == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if entity.Every >= rule.NotificationEvery {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func resetNotifyPolicy(entity *entity.BruteForceProtectionNotifyPolicy) *entity.BruteForceProtectionNotifyPolicy {
|
||||
entity.Every = 0
|
||||
entity.CooldownTime = time.Now().Unix()
|
||||
return entity
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ type group struct {
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Block bool
|
||||
BlockSec uint32
|
||||
BlockConfig brute_force_protection.Block
|
||||
LastLogs []string
|
||||
Block bool
|
||||
BlockSec uint32
|
||||
BlockConfig brute_force_protection.Block
|
||||
BlockIPCount uint64
|
||||
LastLogs []string
|
||||
}
|
||||
|
||||
func NewGroup(groupRepository repository.BruteForceProtectionGroupRepository, logger log.Logger) Group {
|
||||
@@ -36,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
|
||||
@@ -106,6 +107,8 @@ func (g *group) analysisResult(rateLimit brute_force_protection.RateLimit, event
|
||||
entityGroup.CurrentLevelTriggerCount++
|
||||
entityGroup.TriggerCount = 0
|
||||
entityGroup.LastLogs = []string{}
|
||||
|
||||
analysisResult.BlockIPCount = entityGroup.CurrentLevelTriggerCount
|
||||
} else {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection not reached rate limit"))
|
||||
}
|
||||
|
||||
+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()),
|
||||
|
||||
+19
-12
@@ -18,6 +18,7 @@ type Repositories interface {
|
||||
NotificationsQueue() repository.NotificationsQueueRepository
|
||||
AlertGroup() repository.AlertGroupRepository
|
||||
BruteForceProtectionGroup() repository.BruteForceProtectionGroupRepository
|
||||
BruteForceProtectionNotifyPolicy() repository.BruteForceProtectionNotifyPolicyRepository
|
||||
Blocking() repository.BlockingRepository
|
||||
Blocklist() repository.BlocklistRepository
|
||||
Metadata() repository.MetadataRepository
|
||||
@@ -26,12 +27,13 @@ type Repositories interface {
|
||||
}
|
||||
|
||||
type repositories struct {
|
||||
notificationsQueue repository.NotificationsQueueRepository
|
||||
alertGroup repository.AlertGroupRepository
|
||||
bruteForceProtectionGroup repository.BruteForceProtectionGroupRepository
|
||||
blocking repository.BlockingRepository
|
||||
blocklist repository.BlocklistRepository
|
||||
metadata repository.MetadataRepository
|
||||
notificationsQueue repository.NotificationsQueueRepository
|
||||
alertGroup repository.AlertGroupRepository
|
||||
bruteForceProtectionGroup repository.BruteForceProtectionGroupRepository
|
||||
bruteForceProtectionNotifyPolicy repository.BruteForceProtectionNotifyPolicyRepository
|
||||
blocking repository.BlockingRepository
|
||||
blocklist repository.BlocklistRepository
|
||||
metadata repository.MetadataRepository
|
||||
|
||||
db []*bbolt.DB
|
||||
}
|
||||
@@ -57,12 +59,13 @@ func New(dataDir string) (Repositories, error) {
|
||||
securityDB, err := bbolt.Open(dataDir+securityDB, 0600, &bbolt.Options{Timeout: 3 * time.Second})
|
||||
|
||||
return &repositories{
|
||||
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
|
||||
alertGroup: repository.NewAlertGroupRepository(appDB),
|
||||
bruteForceProtectionGroup: repository.NewBruteForceProtectionGroupRepository(securityDB),
|
||||
blocking: repository.NewBlockingRepository(securityDB),
|
||||
blocklist: repository.NewBlocklistRepository(securityDB),
|
||||
metadata: repository.NewMetadataRepository(appDB),
|
||||
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
|
||||
alertGroup: repository.NewAlertGroupRepository(appDB),
|
||||
bruteForceProtectionGroup: repository.NewBruteForceProtectionGroupRepository(securityDB),
|
||||
bruteForceProtectionNotifyPolicy: repository.NewBruteForceProtectionNotifyPolicyRepository(securityDB),
|
||||
blocking: repository.NewBlockingRepository(securityDB),
|
||||
blocklist: repository.NewBlocklistRepository(securityDB),
|
||||
metadata: repository.NewMetadataRepository(appDB),
|
||||
|
||||
db: []*bbolt.DB{appDB, securityDB},
|
||||
}, nil
|
||||
@@ -80,6 +83,10 @@ func (r *repositories) BruteForceProtectionGroup() repository.BruteForceProtecti
|
||||
return r.bruteForceProtectionGroup
|
||||
}
|
||||
|
||||
func (r *repositories) BruteForceProtectionNotifyPolicy() repository.BruteForceProtectionNotifyPolicyRepository {
|
||||
return r.bruteForceProtectionNotifyPolicy
|
||||
}
|
||||
|
||||
func (r *repositories) Blocking() repository.BlockingRepository {
|
||||
return r.blocking
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package entity
|
||||
|
||||
type BruteForceProtectionNotifyPolicy struct {
|
||||
CooldownTime int64
|
||||
Every uint32
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type BruteForceProtectionNotifyPolicyRepository interface {
|
||||
Update(ruleName string, f func(*entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error)) error
|
||||
}
|
||||
|
||||
type bruteForceProtectionNotifyPolicyRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewBruteForceProtectionNotifyPolicyRepository(appDB *bbolt.DB) BruteForceProtectionNotifyPolicyRepository {
|
||||
return &bruteForceProtectionNotifyPolicyRepository{
|
||||
db: appDB,
|
||||
bucket: bruteForceProtectionNotifyPolicyBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *bruteForceProtectionNotifyPolicyRepository) Update(ruleName string, f func(*entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error)) error {
|
||||
entityNotify := &entity.BruteForceProtectionNotifyPolicy{}
|
||||
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := []byte(ruleName)
|
||||
|
||||
notify := b.Get(key)
|
||||
if notify != nil {
|
||||
err = json.Unmarshal(notify, entityNotify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal brute force protection notify policy: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entityNotify, err = f(entityNotify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entityNotify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
notificationsQueueBucket = "notifications_queue"
|
||||
alertGroupBucket = "alert_group"
|
||||
bruteForceProtectionGroupBucket = "brute_force_protection_group"
|
||||
blockingBucket = "blocking"
|
||||
blocklistBucket = "blocklist"
|
||||
metadataBucket = "metadata"
|
||||
notificationsQueueBucket = "notifications_queue"
|
||||
alertGroupBucket = "alert_group"
|
||||
bruteForceProtectionGroupBucket = "brute_force_protection_group"
|
||||
bruteForceProtectionNotifyPolicyBucket = "brute_force_protection_notify_policy"
|
||||
blockingBucket = "blocking"
|
||||
blocklistBucket = "blocklist"
|
||||
metadataBucket = "metadata"
|
||||
)
|
||||
|
||||
func nextID(b *bbolt.Bucket) ([]byte, error) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -194,7 +194,7 @@ func (r *reload) outputICMP(batchOutput chain.Chain) error {
|
||||
func (r *reload) outputICMPAfter(batchOutput chain.Chain) error {
|
||||
if r.config.IP4.IcmpTimestampDrop == true {
|
||||
drop := r.config.Policy.OutputDrop.String()
|
||||
if err := batchOutput.AddRule("oifname != \"lo\" ip protocol icmp icmp type timestamp-request " + drop); err != nil {
|
||||
if err := batchOutput.AddRule("oifname != \"lo\" ip protocol icmp icmp type timestamp-reply " + drop); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -52,6 +52,16 @@
|
||||
"cmd.daemon.geoip.refresh.Description": "GeoIP database update. Must be enabled and configured in geoip.toml. \nExample: \nkor-elf-shield geoip refresh",
|
||||
"geoip_refresh_success": "GeoIP database updated successfully",
|
||||
|
||||
"cmd.daemon.config.Usage": "Configuration",
|
||||
"cmd.daemon.config.test.Usage": "Checking the daemon configuration",
|
||||
"cmd.daemon.config.test.Description": "Checks the daemon configuration.",
|
||||
|
||||
"cmd.daemon.config.test.error": "Error: {{.Error}}",
|
||||
"cmd.daemon.config.test.settingTitle": "Settings:",
|
||||
"cmd.daemon.config.test.main": "Main",
|
||||
"cmd.daemon.config.test.checkingPrograms": "Checking programs:",
|
||||
"cmd.daemon.config.test.pathEmpty": "{{.Program}} path is empty",
|
||||
|
||||
"Command error": "Command error",
|
||||
"invalid log level": "The log level specified in the settings is invalid. It is currently set to: {{.Level}}. Valid values: {{.Levels}}",
|
||||
"invalid log encoding": "Invalid encoding setting. Currently set to: {{.Encoding}}. Valid values: {{.Encodings}}",
|
||||
@@ -71,6 +81,8 @@
|
||||
"unknown": "unknown",
|
||||
"blockSec": "Blocked for {{.BlockSec}}",
|
||||
"ports": "Ports: {{.Ports}}",
|
||||
"alertNumber": "Alert number: {{.Count}}",
|
||||
"blockIPCount": "The number of times the IP address was blocked: {{.Count}}",
|
||||
|
||||
"alert.subject": "Alert detected ({{.Name}}) (group:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "Logged into the OS via ssh.",
|
||||
@@ -84,5 +96,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}}"
|
||||
}
|
||||
|
||||
@@ -52,6 +52,16 @@
|
||||
"cmd.daemon.geoip.refresh.Description": "GeoIP дерекқорын жаңарту. geoip.toml файлында қосулы және конфигурацияланған болуы керек. \nМысал: \nkor-elf-shield geoip refresh",
|
||||
"geoip_refresh_success": "GeoIP дерекқоры сәтті жаңартылды",
|
||||
|
||||
"cmd.daemon.config.Usage": "Конфигурация",
|
||||
"cmd.daemon.config.test.Usage": "Демон конфигурациясын тексеру",
|
||||
"cmd.daemon.config.test.Description": "Демон конфигурациясын тексереді.",
|
||||
|
||||
"cmd.daemon.config.test.error": "Қате: {{.Error}}",
|
||||
"cmd.daemon.config.test.settingTitle": "Параметрлер:",
|
||||
"cmd.daemon.config.test.main": "Негізгі",
|
||||
"cmd.daemon.config.test.checkingPrograms": "Бағдарламаларды тексеру:",
|
||||
"cmd.daemon.config.test.pathEmpty": "{{.Program}} жолы бос",
|
||||
|
||||
"Command error": "Командалық қате",
|
||||
"invalid log level": "Параметрлерде көрсетілген журнал деңгейі жарамсыз. Ол қазір мына күйге орнатылған: {{.Level}}. Жарамды мәндер: {{.Levels}}",
|
||||
"invalid log encoding": "Жарамсыз кодтау параметрі. Қазіргі уақытта орнатылған: {{.Encoding}}. Жарамды мәндер: {{.Encodings}}",
|
||||
@@ -71,6 +81,8 @@
|
||||
"unknown": "белгісіз",
|
||||
"blockSec": "{{.BlockSec}} блокталды",
|
||||
"ports": "Порттар: {{.Ports}}",
|
||||
"alertNumber": "Ескерту нөмірі: {{.Count}}",
|
||||
"blockIPCount": "IP мекенжайының бұғатталған саны: {{.Count}}",
|
||||
|
||||
"alert.subject": "Ескерту анықталды ({{.Name}}) (топ:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "ОС-қа ssh арқылы кірді.",
|
||||
@@ -84,5 +96,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}}"
|
||||
}
|
||||
@@ -52,6 +52,16 @@
|
||||
"cmd.daemon.geoip.refresh.Description": "Обновление базу данных GeoIP. Должно быть включено и настроено в geoip.toml. \nПример: \nkor-elf-shield geoip refresh",
|
||||
"geoip_refresh_success": "База данных GeoIP успешно обновлена",
|
||||
|
||||
"cmd.daemon.config.Usage": "Конфигурация",
|
||||
"cmd.daemon.config.test.Usage": "Проверка конфигурации демона",
|
||||
"cmd.daemon.config.test.Description": "Проверяет конфигурацию демона.",
|
||||
|
||||
"cmd.daemon.config.test.error": "Ошибка: {{.Error}}",
|
||||
"cmd.daemon.config.test.settingTitle": "Настройки:",
|
||||
"cmd.daemon.config.test.main": "Главные",
|
||||
"cmd.daemon.config.test.checkingPrograms": "Проверка программ:",
|
||||
"cmd.daemon.config.test.pathEmpty": "Путь к {{.Program}} пустой",
|
||||
|
||||
"Command error": "Ошибка команды",
|
||||
"invalid log level": "В настройках указан не верный уровень log. Сейчас указан: {{.Level}}. Допустимые значения: {{.Levels}}",
|
||||
"invalid log encoding": "Неверная настройка encoding. Сейчас указан: {{.Encoding}}. Допустимые значения: {{.Encodings}}",
|
||||
@@ -71,6 +81,8 @@
|
||||
"unknown": "неизвестный",
|
||||
"blockSec": "Блокировка на {{.BlockSec}}",
|
||||
"ports": "Порты: {{.Ports}}",
|
||||
"alertNumber": "Номер оповещения: {{.Count}}",
|
||||
"blockIPCount": "Количество раз, когда IP-адрес был заблокирован: {{.Count}}",
|
||||
|
||||
"alert.subject": "Обнаружено оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "Вошли в ОС через ssh.",
|
||||
@@ -84,5 +96,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}}"
|
||||
}
|
||||
@@ -17,3 +17,7 @@ func (l *falseLogger) Sync() error { return nil }
|
||||
func (l *falseLogger) ReOpen() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFalseLogger() Logger {
|
||||
return &falseLogger{}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (l *logger) ReOpen() error {
|
||||
|
||||
func NewLogger(opts LoggerOptions) (Logger, error) {
|
||||
if !opts.Enabled {
|
||||
return &falseLogger{}, nil
|
||||
return NewFalseLogger(), nil
|
||||
}
|
||||
|
||||
return &logger{
|
||||
|
||||
@@ -23,6 +23,8 @@ type BruteForceProtection struct {
|
||||
BlockingTime int `mapstructure:"blocking_time"`
|
||||
SSHEnable bool `mapstructure:"ssh_enable"`
|
||||
SSHNotify bool `mapstructure:"ssh_notify"`
|
||||
SSHNotifyCooldown int `mapstructure:"ssh_notify_cooldown_seconds"`
|
||||
SSHNotifyEvery int `mapstructure:"ssh_notify_every"`
|
||||
SSHGroup string `mapstructure:"ssh_group"`
|
||||
|
||||
Groups []BruteForceProtectionGroup
|
||||
@@ -39,6 +41,8 @@ func defaultBruteForceProtection() BruteForceProtection {
|
||||
BlockingTime: 3600,
|
||||
SSHEnable: true,
|
||||
SSHNotify: true,
|
||||
SSHNotifyCooldown: 0,
|
||||
SSHNotifyEvery: 0,
|
||||
SSHGroup: "",
|
||||
|
||||
Groups: []BruteForceProtectionGroup{},
|
||||
@@ -62,6 +66,15 @@ func (p *BruteForceProtection) Validate() error {
|
||||
if p.BlockingTime < 0 {
|
||||
return errors.New("blocking time must be positive")
|
||||
}
|
||||
|
||||
if p.SSHNotifyCooldown < 0 {
|
||||
return errors.New("ssh notify cooldown must be positive")
|
||||
}
|
||||
|
||||
if p.SSHNotifyEvery < 0 {
|
||||
return errors.New("ssh notify every must be positive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -85,7 +98,7 @@ func (p *BruteForceProtection) ToSources() ([]*config.Source, error) {
|
||||
}
|
||||
sshGroup = p.SSHGroup
|
||||
}
|
||||
sshSources, err := config.NewBruteForceProtectionSSH(p.Notify && p.SSHNotify, groups[sshGroup])
|
||||
sshSources, err := config.NewBruteForceProtectionSSH(p.Notify && p.SSHNotify, p.SSHNotifyCooldown, p.SSHNotifyEvery, groups[sshGroup])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,15 @@ import (
|
||||
)
|
||||
|
||||
type BruteForceProtectionRule struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
Name string `mapstructure:"name"`
|
||||
Message string `mapstructure:"message"`
|
||||
Group string `mapstructure:"group"`
|
||||
Source Source
|
||||
Patterns []BruteForceProtectionPattern
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
NotifyCooldown int `mapstructure:"notify_cooldown_seconds"`
|
||||
NotifyEvery int `mapstructure:"notify_every"`
|
||||
Name string `mapstructure:"name"`
|
||||
Message string `mapstructure:"message"`
|
||||
Group string `mapstructure:"group"`
|
||||
Source Source
|
||||
Patterns []BruteForceProtectionPattern
|
||||
}
|
||||
|
||||
func (l *BruteForceProtectionRule) ToSource(isNotify bool, group *brute_force_protection.Group) (*config.Source, error) {
|
||||
@@ -46,11 +48,13 @@ func (l *BruteForceProtectionRule) ToSource(isNotify bool, group *brute_force_pr
|
||||
}
|
||||
|
||||
source.BruteForceProtectionRule = &brute_force_protection.Rule{
|
||||
Name: l.Name,
|
||||
Message: l.Message,
|
||||
IsNotification: isNotify && l.Notify,
|
||||
Patterns: patterns,
|
||||
Group: group,
|
||||
Name: l.Name,
|
||||
Message: l.Message,
|
||||
IsNotification: isNotify && l.Notify,
|
||||
NotificationCooldown: uint32(l.NotifyCooldown),
|
||||
NotificationEvery: uint32(l.NotifyEvery),
|
||||
Patterns: patterns,
|
||||
Group: group,
|
||||
}
|
||||
|
||||
return source, nil
|
||||
@@ -65,5 +69,13 @@ func (l *BruteForceProtectionRule) validate() error {
|
||||
return fmt.Errorf("brute force protection invalid name: %s", l.Name)
|
||||
}
|
||||
|
||||
if l.NotifyCooldown < 0 {
|
||||
return fmt.Errorf("brute force protection notify cooldown must be positive")
|
||||
}
|
||||
|
||||
if l.NotifyEvery < 0 {
|
||||
return fmt.Errorf("brute force protection notify every must be positive")
|
||||
}
|
||||
|
||||
return 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) {
|
||||
|
||||
+38
-29
@@ -44,31 +44,11 @@ func settingDefault(configPath string) *setting {
|
||||
}
|
||||
|
||||
func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, error) {
|
||||
if s.PidFile == "" {
|
||||
return daemon.DaemonOptions{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "pid_file",
|
||||
}))
|
||||
if err := s.ValidateBeforeStart(); err != nil {
|
||||
return daemon.DaemonOptions{}, err
|
||||
}
|
||||
|
||||
if s.SocketFile == "" {
|
||||
return daemon.DaemonOptions{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "socket_file",
|
||||
}))
|
||||
}
|
||||
|
||||
if s.BinaryLocations.Nftables == "" {
|
||||
return daemon.DaemonOptions{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "binaryLocations.nftables",
|
||||
}))
|
||||
}
|
||||
|
||||
if s.DataDir == "" {
|
||||
return daemon.DaemonOptions{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "data_dir",
|
||||
}))
|
||||
}
|
||||
|
||||
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
|
||||
firewallConfig, guardConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
|
||||
if err != nil {
|
||||
return daemon.DaemonOptions{}, err
|
||||
}
|
||||
@@ -79,15 +59,44 @@ 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
|
||||
}
|
||||
|
||||
func (s setting) ValidateBeforeStart() error {
|
||||
if s.PidFile == "" {
|
||||
return errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "pid_file",
|
||||
}))
|
||||
}
|
||||
|
||||
if s.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "socket_file",
|
||||
}))
|
||||
}
|
||||
|
||||
if s.BinaryLocations.Nftables == "" {
|
||||
return errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "binaryLocations.nftables",
|
||||
}))
|
||||
}
|
||||
|
||||
if s.DataDir == "" {
|
||||
return errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||
"Parameter": "data_dir",
|
||||
}))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s setting) Validate() error {
|
||||
if err := s.validationTestingInterval(); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user