Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5ff0c1afd | |||
|
63bc845b8b
|
|||
|
a44f9b4e75
|
|||
|
4647d1303e
|
|||
|
221fdb8d3b
|
|||
|
a7e4c7d750
|
|||
|
75c8eba0cd
|
|||
|
bf8711aadd
|
|||
|
1dbb4d0bff
|
|||
|
993f48f541
|
|||
|
286f32b618
|
|||
|
42e4a8cf40
|
|||
|
be3861ee6e
|
|||
|
d0a358a445
|
|||
|
39cfb8a7b6
|
|||
|
65eaa37637
|
|||
| c4852c3540 | |||
|
b884494250
|
|||
|
598d83d6da
|
|||
|
f737edc3ce
|
|||
|
dc85bc759a
|
|||
|
93b2927da7
|
|||
|
afb0773dfd
|
|||
|
187c447301
|
|||
|
3ec6b4c72d
|
|||
|
b63e3adbd3
|
|||
|
aa519c8b44
|
|||
|
8329da32e3
|
|||
|
833bc394b3
|
|||
|
e422bc4206
|
|||
|
47aa0a9d6c
|
|||
|
58dbee450a
|
|||
|
68034fd6f9
|
|||
|
7b77b8730e
|
|||
|
187e874c29
|
|||
|
ee5a6a2d3d
|
|||
|
38283247e9
|
|||
|
79c7ef1f91
|
|||
|
e29d0de632
|
|||
|
be082a1841
|
|||
|
4b364cbdf0
|
|||
|
dfa23bc7a6
|
|||
|
3a34569e78
|
|||
|
b1f5ce4e9b
|
|||
|
f2d851baa7
|
|||
|
2a617b5c17
|
|||
|
a648647e4a
|
|||
|
6b482a350b
|
|||
|
097cf362e3
|
|||
|
bf7d463930
|
|||
|
b49889ef58
|
|||
|
fd899087d4
|
|||
|
8f254d11c1
|
|||
|
2e08bf6b6a
|
|||
|
036f037a30
|
|||
|
c7f25b4ba8
|
|||
|
623d626878
|
|||
|
e1bace602c
|
|||
|
e85fd785cd
|
|||
|
c6841d14f3
|
|||
|
57b80da767
|
|||
|
696961f7c0
|
|||
|
af082f36da
|
|||
|
a889e5c81a
|
|||
|
99e155fe10
|
|||
|
2fffe45a89
|
|||
|
ff0317ed0b
|
|||
|
0b627a283d
|
|||
|
2b8a3e0d98
|
|||
|
c09bf01de1
|
|||
|
627b70e024
|
|||
|
660e1fcebd
|
|||
|
c9093f8244
|
|||
|
8985ff884d
|
|||
|
c7dadb3684
|
|||
|
d5e92b70ef
|
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,3 +1,39 @@
|
||||
## 0.8.0 (9.3.2026)
|
||||
* Теперь можно тонко настроить блокировку портов для IP адреса, который пытается подобрать пароль.
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups]] добавлен новый параметр "block_type".
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups]] добавлен новый параметр "ports".
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups.rate_limits]] добавлен новый параметр "block_type".
|
||||
* В файл настроек analyzer.toml в [[bruteForceProtection.groups.rate_limits]] добавлен новый параметр "ports".
|
||||
* Смотрите полный список по ссылке: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/d0a358a445b1dec850d8b84c06e86bd6872796cf/assets/configs/analyzer.toml
|
||||
* Команда `kor-elf-shield ban clear` была переименованна в `kor-elf-shield block clear`.
|
||||
* Добавлена команда `kor-elf-shield block add`. Через эту команду можно заблокировать IP адрес. Смотрите подробно в `kor-elf-shield block add --help`.
|
||||
* Добавлена команда `kor-elf-shield block delete`. Через эту команду можно удалить заблокированный IP адрес. Смотрите подробно в `kor-elf-shield block delete --help`.
|
||||
***
|
||||
#### English
|
||||
* You can now fine-tune port blocking for the IP address attempting to brute-force a password.
|
||||
* A new "block_type" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups]].
|
||||
* A new "ports" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups]].
|
||||
* A new "block_type" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups.rate_limits]].
|
||||
* A new "ports" parameter has been added to the analyzer.toml settings file in [[bruteForceProtection.groups.rate_limits]].
|
||||
* See the full list at: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/d0a358a445b1dec850d8b84c06e86bd6872796cf/assets/configs/analyzer.toml
|
||||
* The `kor-elf-shield ban clear` command has been renamed to `kor-elf-shield block clear`.
|
||||
* 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)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена возможность настройки отслеживания событий в журналах.
|
||||
* Добавлены настройки для защиты от перебора паролей.
|
||||
* В файл настроек analyzer.toml добавлены новые параметры. Смотрите полный список по ссылке: https://git.kor-elf.net/kor-elf-shield/kor-elf-shield/src/commit/187c447301b9c0bfa41ec2b2c9435ab0ce44bed6/assets/configs/analyzer.toml
|
||||
* Добавлена команда `kor-elf-shield ban clear`, которая разблокирует все IP адреса. Которые были забанены.
|
||||
***
|
||||
#### English
|
||||
* Added the ability to customize event tracking in logs.
|
||||
* Added settings to protect against password guessing.
|
||||
* 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)
|
||||
***
|
||||
#### Русский
|
||||
|
||||
@@ -92,6 +92,32 @@ THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/nxadm/tail
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# © Copyright 2015 Hewlett Packard Enterprise Development LP
|
||||
Copyright (c) 2014 ActiveState
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/pelletier/go-toml/v2
|
||||
|
||||
The bulk of github.com/pelletier/go-toml is distributed under the MIT license
|
||||
@@ -873,6 +899,40 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
gopkg.in/tomb.v1
|
||||
|
||||
tomb - support for clean goroutine termination in Go.
|
||||
|
||||
Copyright (c) 2010-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
|
||||
All rights reserved.
|
||||
|
||||
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 the copyright holder 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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
github.com/golang/go
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
@@ -904,4 +964,3 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
* Подружить с docker (частично).
|
||||
* Внедрить настройку уведомлений (пока только e-mail).
|
||||
* Отправлять уведомления при авторизации ssh.
|
||||
* Защита от перебора паролей (brute-force).
|
||||
|
||||
### В планах:
|
||||
* Защита от перебора паролей (brute-force).
|
||||
* Уведомлять, если появится новый пользователь в системе.
|
||||
* Уведомлять, если изменились системные файлы.
|
||||
***
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
* Make friends with docker (partially).
|
||||
* Implement notification settings (for now only by e-mail).
|
||||
* Send notifications during ssh authorization.
|
||||
* Password brute-force protection.
|
||||
|
||||
### The plans include:
|
||||
* Password brute-force protection.
|
||||
* Notify if a new user appears in the system.
|
||||
* Notify if system files have changed.
|
||||
***
|
||||
|
||||
@@ -1,3 +1,206 @@
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Защита от перебора пароля
|
||||
# ***
|
||||
# SECTION:Protection against password brute-force attacks
|
||||
###############################################################################
|
||||
[bruteForceProtection]
|
||||
###
|
||||
# Включает группу отслеживания перебора пароля.
|
||||
# Если отключено, отслеживание перебора пароля работать не будет.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables the password attack monitoring group.
|
||||
# If disabled, password attack monitoring will not work.
|
||||
# Default: true
|
||||
###
|
||||
enabled = true
|
||||
|
||||
###
|
||||
# Включает уведомления об блокировок.
|
||||
# Если отключено, они будут отображаться в логах только на уровне = "info".
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications about blocks.
|
||||
# If disabled, they will only appear in the logs under level = "info".
|
||||
# Default: true
|
||||
###
|
||||
notify = true
|
||||
|
||||
###
|
||||
# Максимальное количество ошибок, после которого произойдёт блокировка.
|
||||
# По умолчанию: 5
|
||||
# ***
|
||||
# The maximum number of errors after which a blocking will occur.
|
||||
# Default: 5
|
||||
###
|
||||
rate_limit_count = 5
|
||||
|
||||
###
|
||||
# Насколько времени в секундах блокировать IP адрес.
|
||||
# Если указать 0, то будет на всегда заблокирован.
|
||||
# По умолчанию: 3600
|
||||
# ***
|
||||
# How long in seconds to block an IP address.
|
||||
# If you specify 0, it will be blocked forever.
|
||||
# Default: 3600
|
||||
###
|
||||
blocking_time = 3600
|
||||
|
||||
###
|
||||
# Установите временной интервал для отслеживания сбоев входа в систему в течение секунд.
|
||||
# По умолчанию: 3600
|
||||
# ***
|
||||
# Set the time interval to monitor login failures in seconds.
|
||||
# Default: 3600
|
||||
###
|
||||
rate_limit_period = 3600
|
||||
|
||||
###
|
||||
# Указываем в секундах, через какое время сбрасывать данные IP в групе _default если не было событий.
|
||||
# Если указать 0, то не будет сбрасывать.
|
||||
# По умолчанию: 86400
|
||||
# ***
|
||||
# Specify the number of seconds after which IP data in the _default group will be reset if there have been no events.
|
||||
# If you specify 0, the reset will not occur.
|
||||
# Default: 86400
|
||||
###
|
||||
rate_limit_reset_period = 86400
|
||||
|
||||
###
|
||||
# Включает защиту от перебора пароля от ssh.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables protection against brute-force attacks against ssh.
|
||||
# Default: true
|
||||
###
|
||||
ssh_enable = true
|
||||
|
||||
###
|
||||
# Включает уведомления об блокировках, когда срабатывает защита от перебора пароля.
|
||||
# Если отключено, они будут отображаться в логах только на уровне = "info".
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables block notifications when password brute-force protection is triggered.
|
||||
# If disabled, they will only appear in the logs under level = "info".
|
||||
# Default: true
|
||||
###
|
||||
ssh_notify = true
|
||||
|
||||
###
|
||||
# Можно указать свою группу, чтобы связать с другими правилами.
|
||||
# По умолчанию: ""
|
||||
# ***
|
||||
# You can specify your own group to link it to other rules.
|
||||
# Default: ""
|
||||
###
|
||||
ssh_group = ""
|
||||
|
||||
###
|
||||
# Указываем настройки группы.
|
||||
# Примеры:
|
||||
# [[bruteForceProtection.groups]]
|
||||
# name = "my_name_group" # Имя группы. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Любой текст группы" # Текст уведомления (обязательное поле)
|
||||
# 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]]
|
||||
## Через сколько будет срабатывать блокировка. В данном случае в течение часа, если было 5 обнаружений, то сработает блокировка.
|
||||
## И заблокирует на 10 минут.
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# blocking_time = 600
|
||||
## Внутри bruteForceProtection.groups.rate_limits можно переопределить настройки block_type и ports.
|
||||
## block_type = "ip_port" # Указываем тип блокировки: ip, ip_port. Если ничего не укажите, будет указан тип ip.
|
||||
## ports = ["22/tcp", "22/udp", "80/tcp", "443/tcp"] # Если тип блокировки стоит ip_port, то нужно указать порты, которые будут заблокированы после обнаружения попытки перебора пароля.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## После срабатывания блокировки, переходим на второй уровень, тепер если в течение часа было 3 обнаружений, то сработает блокировка.
|
||||
## И теперь заблокирует на час.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# blocking_time = 3600
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## И таких уровней можно указыват сколько захотите.
|
||||
# count = 2
|
||||
# period = 600
|
||||
# blocking_time = 3600
|
||||
#
|
||||
# ***
|
||||
# Specify group settings.
|
||||
# Examples:
|
||||
# [[bruteForceProtection.groups]]
|
||||
# name = "my_name_group" # Group name. Allowed characters are "a-z, A-Z, -, _". The first character must be a letter (required)
|
||||
# message = "Any group text" # Notification text (required)
|
||||
# rate_limit_reset_period = 86400 # Specify, in seconds, how long to reset group data if there have been no events. Specifying 0 means no reset.
|
||||
## block_type = "ip_port" # Specify the blocking type: IP, IP_port. If you don't specify anything, the IP type will be used.
|
||||
## ports = ["22/tcp", "22/udp"] # If the blocking type is ip_port, then you need to specify the ports that will be blocked after detecting a password brute-force attempt.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## How long will it take for the block to be triggered? In this case, if there were 5 detections within an hour, the block will be triggered.
|
||||
## And it will block for 10 minutes.
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# blocking_time = 600
|
||||
## Inside bruteForceProtection.groups.rate_limits you can override the block_type and ports settings.
|
||||
## block_type = "ip_port" # Specify the blocking type: IP, IP_port. If you don't specify anything, the IP type will be used.
|
||||
## ports = ["22/tcp", "22/udp", "80/tcp", "443/tcp"] # If the blocking type is ip_port, then you need to specify the ports that will be blocked after detecting a password brute-force attempt.
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## After the blocking is triggered, we move to the second level. Now, if there are three detections within an hour, the blocking will be triggered.
|
||||
## And now it will block for an hour.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# blocking_time = 3600
|
||||
# [[bruteForceProtection.groups.rate_limits]]
|
||||
## You can specify as many of these levels as you like.
|
||||
# count = 2
|
||||
# period = 600
|
||||
# blocking_time = 3600
|
||||
###
|
||||
|
||||
###
|
||||
# Указываем настройки логов, которые надо отслеживать для защиты от перебора пароля.
|
||||
# Примеры:
|
||||
# [[bruteForceProtection.rules]]
|
||||
# enabled = true # Включает или выключает отслеживания (обязательное поле)
|
||||
# notify = true # Включает или выключает уведомления (обязательное поле)
|
||||
# name = "my_name_rule" # Имя уведомления. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Ваш любой текст для уведомления" # Текст уведомления (обязательное поле)
|
||||
# group = "my_name_group" # Можно указать имя группы (не обязательное поле)
|
||||
# [bruteForceProtection.rules.source]
|
||||
# type = "journalctl" # journalctl или 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
|
||||
# [[bruteForceProtection.rules.patterns]]
|
||||
# regexp = '^Failed password for (\S+) from (\S+) port \S+'
|
||||
# ip = 2 # Указываем номер value, который укажет IP (обязательное поле)
|
||||
# [[bruteForceProtection.rules.patterns.values]]
|
||||
# name = "Пользователь"
|
||||
# value = 1
|
||||
#
|
||||
# ***
|
||||
# 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)
|
||||
# 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)
|
||||
# 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
|
||||
# [[bruteForceProtection.rules.patterns]]
|
||||
# regexp = '^Accepted (\S+) for (\S+) from (\S+) port \S+'
|
||||
# [[bruteForceProtection.rules.patterns]]
|
||||
# regexp = '^Failed password for (\S+) from (\S+) port \S+'
|
||||
# ip = 2 # We indicate the value number that will indicate the IP (required field)
|
||||
# [[bruteForceProtection.rules.patterns.values]]
|
||||
# name = "User"
|
||||
# value = 1
|
||||
###
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Отслеживать авторизаций
|
||||
# ***
|
||||
@@ -108,3 +311,118 @@ sudo_enable = false
|
||||
###
|
||||
sudo_notify = true
|
||||
|
||||
###############################################################################
|
||||
# РАЗДЕЛ:Настройки анализа логов для уведомления
|
||||
# ***
|
||||
# SECTION:Log analysis settings for notifications
|
||||
###############################################################################
|
||||
[logAlert]
|
||||
###
|
||||
# Включает группу отслеживания логов для оповещения.
|
||||
# Если отключено, отслеживание логов для оповещения работать не будет.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables the log monitoring group for alerts.
|
||||
# If disabled, log monitoring for alerts will not work.
|
||||
# Default: true
|
||||
###
|
||||
enabled = true
|
||||
|
||||
###
|
||||
# Включает уведомления.
|
||||
# Если отключено, они будут отображаться в логах только на уровне = "info".
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications.
|
||||
# If disabled, they will only appear in the logs under level = "info".
|
||||
# Default: true
|
||||
###
|
||||
notify = true
|
||||
|
||||
###
|
||||
# Указываем настройки группы.
|
||||
# Примеры:
|
||||
# [[logAlert.groups]]
|
||||
# name = "my_name_group" # Имя группы. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Любой текст группы" # Текст уведомления (обязательное поле)
|
||||
# rate_limit_reset_period = 86400 # Указываем в секундах, через какое время сбрасывать данные в групе если не было событий. Если указать 0, то не будет сбрасывать.
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## Через сколько будет срабатывать оповещение. В данном случае в течение часа, если было 5 обнаружений, то сработает оповещение.
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## После срабатывания оповещения, переходим на второй уровень, тепер если в течение часа было 3 обнаружений, то сработает оповещение.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## И таких уровней можно указыват сколько захотите.
|
||||
# count = 2
|
||||
# period = 600
|
||||
#
|
||||
# ***
|
||||
# Specify group settings.
|
||||
# Examples:
|
||||
# [[logAlert.groups]]
|
||||
# name = "my_name_group" # Group name. Allowed characters are "a-z, A-Z, -, _". The first character must be a letter (required)
|
||||
# message = "Any group text" # Notification text (required)
|
||||
# rate_limit_reset_period = 86400 # Specify, in seconds, how long to reset group data if there have been no events. Specifying 0 means no reset.
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## How long to wait before an alert is triggered. In this case, if there were 5 detections within an hour, the alert will be triggered. # count = 5
|
||||
# count = 5
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## After the alert is triggered, we move to the second level. Now, if there are 3 detections within an hour, the alert will be triggered.
|
||||
# count = 3
|
||||
# period = 3600
|
||||
# [[logAlert.groups.rate_limits]]
|
||||
## You can specify as many of these levels as you like.
|
||||
# count = 2
|
||||
# period = 600
|
||||
###
|
||||
|
||||
###
|
||||
# Указываем настройки логов, которые надо отслеживать для оповещения.
|
||||
# Примеры:
|
||||
# [[logAlert.rules]]
|
||||
# enabled = true # Включает или выключает отслеживания (обязательное поле)
|
||||
# notify = true # Включает или выключает уведомления (обязательное поле)
|
||||
# name = "my_name_rule" # Имя уведомления. Разрешены символы "a-z, A-Z, -, _". Первый символ обязательно буква (обязательное поле)
|
||||
# message = "Ваш любой текст для уведомления" # Текст уведомления (обязательное поле)
|
||||
# group = "my_name_group" # Можно указать имя группы (не обязательное поле)
|
||||
# [logAlert.rules.source]
|
||||
# type = "journalctl" # journalctl или 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
|
||||
# [[logAlert.rules.patterns]]
|
||||
# regexp = '^Accepted (\S+) for (\S+) from (\S+) port \S+'
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "Пользователь"
|
||||
# value = 2
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "IP"
|
||||
# value = 3
|
||||
#
|
||||
# ***
|
||||
# Specify the log settings to monitor for notifications.
|
||||
# Examples:
|
||||
# [[logAlert.rules]]
|
||||
# enabled = true # Enables or disables tracking (required)
|
||||
# notify = true # Enables or disables notifications (required)
|
||||
# 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)
|
||||
# [logAlert.rules.source]
|
||||
# type = "journalctl" # journalctl or file (required)
|
||||
# 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
|
||||
# [[logAlert.rules.patterns]]
|
||||
# regexp = '^Accepted (\S+) for (\S+) from (\S+) port \S+'
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "User"
|
||||
# value = 2
|
||||
# [[logAlert.rules.patterns.values]]
|
||||
# name = "IP"
|
||||
# value = 3
|
||||
###
|
||||
|
||||
16
go.mod
16
go.mod
@@ -4,26 +4,28 @@ go 1.25
|
||||
|
||||
require (
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
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.4.1
|
||||
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.0
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.org/x/text v0.29.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/text v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // 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.10.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
)
|
||||
|
||||
22
go.sum
22
go.sum
@@ -2,14 +2,18 @@ git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1 h1:3oGtZ/r1YAdlvI16OkZS
|
||||
git.kor-elf.net/kor-elf-shield/go-nftables-client v0.1.1/go.mod h1:a7F+XdL1pK5P3ucQRR2EK/fABAP37LLBENiA4hX7L6A=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
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/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -18,6 +22,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -40,6 +48,8 @@ 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.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
|
||||
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||
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/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
@@ -48,18 +58,30 @@ 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.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
141
internal/cmd/daemon/block.go
Normal file
141
internal/cmd/daemon/block.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CmdBlock() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "block",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.Usage"),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.block.add.Description"),
|
||||
Action: cmdBlockAdd,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "port",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.FlagUsage.port"),
|
||||
},
|
||||
&cli.Uint32Flag{
|
||||
Name: "seconds",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.FlagUsage.seconds"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "reason",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.add.FlagUsage.reason"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.delete.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.block.delete.Description"),
|
||||
Action: cmdBlockDelete,
|
||||
},
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: i18n.Lang.T("cmd.daemon.block.clear.Usage"),
|
||||
Description: i18n.Lang.T("cmd.daemon.block.clear.Description"),
|
||||
Action: cmdBlockClear,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cmdBlockAdd(_ context.Context, cmd *cli.Command) error {
|
||||
ip := net.ParseIP(cmd.Args().Get(0))
|
||||
if ip == nil {
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.SendCommand("block_add_ip", map[string]string{
|
||||
"ip": ip.String(),
|
||||
"port": cmd.String("port"),
|
||||
"seconds": strconv.Itoa(int(cmd.Uint32("seconds"))),
|
||||
"reason": cmd.String("reason"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("cmd.error", map[string]any{
|
||||
"Error": result,
|
||||
}))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("block_add_ip_success"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdBlockDelete(_ context.Context, cmd *cli.Command) error {
|
||||
ip := net.ParseIP(cmd.Args().Get(0))
|
||||
if ip == nil {
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.SendCommand("block_delete_ip", map[string]string{
|
||||
"ip": ip.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("cmd.error", map[string]any{
|
||||
"Error": result,
|
||||
}))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("block_delete_ip_success"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdBlockClear(_ context.Context, _ *cli.Command) error {
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
defer func() {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
result, err := sock.Send("block_clear")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result != "ok" {
|
||||
return errors.New(i18n.Lang.T("block_clear_error"))
|
||||
}
|
||||
|
||||
fmt.Println(i18n.Lang.T("block_clear_success"))
|
||||
|
||||
return nil
|
||||
}
|
||||
16
internal/cmd/daemon/main.go
Normal file
16
internal/cmd/daemon/main.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
)
|
||||
|
||||
func newSocket() (socket.Client, error) {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return nil, errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
return socket.NewSocketClient(setting.Config.SocketFile)
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -39,10 +37,7 @@ func CmdNotifications() *cli.Command {
|
||||
}
|
||||
|
||||
func cmdNotificationsQueueCount(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
@@ -63,10 +58,7 @@ func cmdNotificationsQueueCount(_ context.Context, _ *cli.Command) error {
|
||||
}
|
||||
|
||||
func cmdNotificationsQueueClear(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -21,11 +19,7 @@ func CmdReopenLogger() *cli.Command {
|
||||
}
|
||||
|
||||
func cmdReopenLogger(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
||||
defer func() {
|
||||
_ = repositories.Close()
|
||||
}()
|
||||
config.Repositories = repositories
|
||||
|
||||
notificationsService, err := newNotificationsService(repositories.NotificationsQueue(), logger)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -21,11 +19,7 @@ func CmdStatus() *cli.Command {
|
||||
}
|
||||
|
||||
func cmdStatus(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/socket"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -21,11 +19,7 @@ func CmdStop() *cli.Command {
|
||||
}
|
||||
|
||||
func stopDaemon(_ context.Context, _ *cli.Command) error {
|
||||
if setting.Config.SocketFile == "" {
|
||||
return errors.New(i18n.Lang.T("socket file is not specified"))
|
||||
}
|
||||
|
||||
sock, err := socket.NewSocketClient(setting.Config.SocketFile)
|
||||
sock, err := newSocket()
|
||||
if err != nil {
|
||||
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ func NewMainApp(appVer AppVersion, defaultConfigPath string) *cli.Command {
|
||||
daemon.CmdStatus(),
|
||||
daemon.CmdReopenLogger(),
|
||||
daemon.CmdNotifications(),
|
||||
daemon.CmdBlock(),
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
@@ -7,12 +7,15 @@ import (
|
||||
config2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
analyzerLog "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log"
|
||||
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/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/notifications"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type Analyzer interface {
|
||||
Run(ctx context.Context)
|
||||
ClearDBData() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -21,39 +24,56 @@ type analyzer struct {
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
systemd analyzerLog.Systemd
|
||||
files analyzerLog.FileMonitoring
|
||||
analysis analyzerLog.Analysis
|
||||
|
||||
logChan chan analysisServices.Entry
|
||||
}
|
||||
|
||||
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||
var matches []string
|
||||
alertRuleIndex := analysisServices.NewAlertRuleIndex()
|
||||
func New(config config2.Config, blockService brute_force_protection_group.BlockService, repositories db.Repositories, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||
var journalMatches []string
|
||||
journalMatchesUniq := map[string]struct{}{}
|
||||
|
||||
var files []string
|
||||
filesUniq := map[string]struct{}{}
|
||||
|
||||
rulesIndex := analysisServices.NewRulesIndex()
|
||||
|
||||
for _, source := range config.Sources {
|
||||
switch source.Type {
|
||||
case config2.SourceTypeJournal:
|
||||
match := source.Journal.JournalctlMatch()
|
||||
matches = append(matches, match)
|
||||
if _, ok := journalMatchesUniq[match]; !ok {
|
||||
journalMatchesUniq[match] = struct{}{}
|
||||
journalMatches = append(journalMatches, match)
|
||||
}
|
||||
case config2.SourceTypeFile:
|
||||
file := source.File.Path
|
||||
if _, ok := filesUniq[file]; !ok {
|
||||
filesUniq[file] = struct{}{}
|
||||
files = append(files, file)
|
||||
}
|
||||
default:
|
||||
logger.Error(fmt.Sprintf("Unknown source type: %s", source.Type))
|
||||
continue
|
||||
}
|
||||
|
||||
err := alertRuleIndex.Add(source)
|
||||
err := rulesIndex.Add(source)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add alert rule: %s", err))
|
||||
logger.Error(fmt.Sprintf("Failed to add rule: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, matches, logger)
|
||||
analysisService := analyzerLog.NewAnalysis(alertRuleIndex, logger, notify)
|
||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, journalMatches, logger)
|
||||
filesService := analyzerLog.NewFileMonitoring(files, logger)
|
||||
analysisService := analyzerLog.NewAnalysis(rulesIndex, blockService, repositories, logger, notify)
|
||||
|
||||
return &analyzer{
|
||||
config: config,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
systemd: systemdService,
|
||||
files: filesService,
|
||||
analysis: analysisService,
|
||||
|
||||
logChan: make(chan analysisServices.Entry, 1000),
|
||||
@@ -61,11 +81,28 @@ func New(config config2.Config, logger log.Logger, notify notifications.Notifica
|
||||
}
|
||||
|
||||
func (a *analyzer) Run(ctx context.Context) {
|
||||
go a.systemd.Run(ctx, a.logChan)
|
||||
go a.processLogs(ctx)
|
||||
go a.systemd.Run(ctx, a.logChan)
|
||||
go a.files.Run(ctx, a.logChan)
|
||||
|
||||
a.logger.Debug("Analyzer is start")
|
||||
}
|
||||
|
||||
func (a *analyzer) ClearDBData() error {
|
||||
a.logger.Debug("Clear data")
|
||||
|
||||
clearDBErrors, err := a.analysis.ClearDBData()
|
||||
if err != nil {
|
||||
for _, err := range clearDBErrors {
|
||||
a.logger.Error(err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *analyzer) processLogs(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
@@ -78,6 +115,7 @@ func (a *analyzer) processLogs(ctx context.Context) {
|
||||
}
|
||||
a.logger.Debug(fmt.Sprintf("Received log entry: %v", entry))
|
||||
|
||||
a.analysis.BruteForceProtection(&entry)
|
||||
a.analysis.Alert(&entry)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +123,10 @@ func (a *analyzer) processLogs(ctx context.Context) {
|
||||
|
||||
func (a *analyzer) Close() error {
|
||||
if err := a.systemd.Close(); err != nil {
|
||||
return err
|
||||
a.logger.Error(err.Error())
|
||||
}
|
||||
if err := a.files.Close(); err != nil {
|
||||
a.logger.Error(err.Error())
|
||||
}
|
||||
close(a.logChan)
|
||||
|
||||
|
||||
31
internal/daemon/analyzer/config/alert_group.go
Normal file
31
internal/daemon/analyzer/config/alert_group.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type RateLimit struct {
|
||||
Count uint32
|
||||
Period uint32
|
||||
}
|
||||
|
||||
type AlertGroup struct {
|
||||
Name string
|
||||
Message string
|
||||
RateLimits []RateLimit
|
||||
RateLimitResetPeriod uint32
|
||||
}
|
||||
|
||||
func (g *AlertGroup) RateLimit(level uint64) (rateLimit RateLimit, err error) {
|
||||
lenRateLimits := len(g.RateLimits) - 1
|
||||
|
||||
if lenRateLimits < 0 {
|
||||
return RateLimit{}, fmt.Errorf("rate limits is empty")
|
||||
}
|
||||
|
||||
if level <= uint64(lenRateLimits) {
|
||||
rateLimit = g.RateLimits[level]
|
||||
} else {
|
||||
rateLimit = g.RateLimits[lenRateLimits]
|
||||
}
|
||||
|
||||
return rateLimit, nil
|
||||
}
|
||||
45
internal/daemon/analyzer/config/brute_force_protection.go
Normal file
45
internal/daemon/analyzer/config/brute_force_protection.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/i18n"
|
||||
"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) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSystemdUnit, "ssh.service")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for SSH: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
BruteForceProtectionRule: &brute_force_protection.Rule{
|
||||
Name: "_ssh",
|
||||
Message: i18n.Lang.T("alert.bruteForceProtection.ssh.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []brute_force_protection.RegexPattern{
|
||||
{
|
||||
Regexp: regular_expression.NewLazyRegexp(`^Failed password for (\S+) from (\S+) port \S+`),
|
||||
Values: []brute_force_protection.PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
IP: 2,
|
||||
},
|
||||
},
|
||||
Group: group,
|
||||
},
|
||||
}
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package brute_force_protection
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
|
||||
type Block interface {
|
||||
PortsBlocked() (bool, []types.L4Port)
|
||||
}
|
||||
|
||||
type block struct {
|
||||
shouldPortsBlocked bool
|
||||
ports []types.L4Port
|
||||
}
|
||||
|
||||
func NewBlockOnceIPConfig() Block {
|
||||
return &block{
|
||||
shouldPortsBlocked: false,
|
||||
ports: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBlockIPAndPortsConfig(ports []types.L4Port) Block {
|
||||
return &block{
|
||||
shouldPortsBlocked: true,
|
||||
ports: ports,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *block) PortsBlocked() (bool, []types.L4Port) {
|
||||
if !b.shouldPortsBlocked {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, b.ports
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package brute_force_protection
|
||||
|
||||
import "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
|
||||
}
|
||||
|
||||
type RegexPattern struct {
|
||||
Regexp *regular_expression.LazyRegexp
|
||||
Values []PatternValue
|
||||
IP uint8
|
||||
}
|
||||
|
||||
type RateLimit struct {
|
||||
Count uint32
|
||||
Period uint32
|
||||
BlockingTimeSeconds uint32
|
||||
BlockConfig Block
|
||||
}
|
||||
|
||||
type PatternValue struct {
|
||||
Name string
|
||||
Value uint8
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package brute_force_protection
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Group struct {
|
||||
Name string
|
||||
Message string
|
||||
RateLimits []RateLimit
|
||||
RateLimitResetPeriod uint32
|
||||
}
|
||||
|
||||
func (g *Group) RateLimit(level uint64) (rateLimit RateLimit, err error) {
|
||||
lenRateLimits := len(g.RateLimits) - 1
|
||||
|
||||
if lenRateLimits < 0 {
|
||||
return RateLimit{}, fmt.Errorf("rate limits is empty")
|
||||
}
|
||||
|
||||
if level <= uint64(lenRateLimits) {
|
||||
rateLimit = g.RateLimits[level]
|
||||
} else {
|
||||
rateLimit = g.RateLimits[lenRateLimits]
|
||||
}
|
||||
|
||||
return rateLimit, nil
|
||||
}
|
||||
@@ -1,15 +1,26 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
"strings"
|
||||
"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/pkg/regular_expression"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
var (
|
||||
reSystemdUnitValue = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._@-]{0,255}\.(service|socket|target|mount|timer|path|scope|slice|device)$`)
|
||||
reSyslogIDValue = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._@-]{0,127}$`)
|
||||
)
|
||||
|
||||
type SourceType string
|
||||
|
||||
const (
|
||||
SourceTypeJournal SourceType = "journalctl"
|
||||
SourceTypeFile SourceType = "file"
|
||||
)
|
||||
|
||||
type JournalField string
|
||||
@@ -29,15 +40,67 @@ type SourceJournal struct {
|
||||
Match string
|
||||
}
|
||||
|
||||
func NewSourceJournal(field JournalField, match string) (*SourceJournal, error) {
|
||||
v := strings.TrimSpace(match)
|
||||
if v == "" {
|
||||
return nil, fmt.Errorf("journal match must not be empty")
|
||||
}
|
||||
if len(v) > 512 {
|
||||
return nil, fmt.Errorf("journal match is too long: %d", len(v))
|
||||
}
|
||||
for _, r := range v {
|
||||
if r == 0 || r == '\n' || r == '\r' || unicode.IsControl(r) {
|
||||
return nil, fmt.Errorf("journal match contains control characters")
|
||||
}
|
||||
}
|
||||
// to avoid breaking the FIELD=VALUE format and concatenation with '+'
|
||||
if strings.ContainsAny(v, "=+") {
|
||||
return nil, fmt.Errorf("journal match must not contain '=' or '+'")
|
||||
}
|
||||
|
||||
if strings.ContainsAny(v, " \t") {
|
||||
return nil, fmt.Errorf("journal match must not contain spaces or tabs")
|
||||
}
|
||||
|
||||
switch field {
|
||||
case JournalFieldSystemdUnit:
|
||||
if !reSystemdUnitValue.MatchString(v) {
|
||||
return nil, fmt.Errorf("invalid _SYSTEMD_UNIT value: %q", v)
|
||||
}
|
||||
case JournalFieldSyslogIdentifier:
|
||||
if !reSyslogIDValue.MatchString(v) {
|
||||
return nil, fmt.Errorf("invalid SYSLOG_IDENTIFIER value: %q", v)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid journal field: %q", field)
|
||||
}
|
||||
|
||||
return &SourceJournal{Field: field, Match: v}, nil
|
||||
}
|
||||
|
||||
type SourceFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewSourceFile(path string) (*SourceFile, error) {
|
||||
if err := validate.PathFile(path, "logAlert.rules.source.path"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SourceFile{Path: path}, nil
|
||||
}
|
||||
|
||||
func (s *SourceJournal) JournalctlMatch() string {
|
||||
return string(s.Field) + "=" + s.Match
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Type SourceType
|
||||
Type SourceType
|
||||
Journal *SourceJournal
|
||||
File *SourceFile
|
||||
|
||||
Journal *SourceJournal
|
||||
AlertRule *AlertRule
|
||||
AlertRule *AlertRule
|
||||
BruteForceProtectionRule *brute_force_protection.Rule
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
@@ -49,42 +112,11 @@ type AlertRule struct {
|
||||
}
|
||||
|
||||
type AlertRegexPattern struct {
|
||||
Regexp *LazyRegexp
|
||||
Regexp *regular_expression.LazyRegexp
|
||||
Values []PatternValue
|
||||
}
|
||||
|
||||
type LazyRegexp struct {
|
||||
pattern string
|
||||
|
||||
once sync.Once
|
||||
re *regexp.Regexp
|
||||
err error
|
||||
}
|
||||
|
||||
func NewLazyRegexp(pattern string) *LazyRegexp {
|
||||
return &LazyRegexp{pattern: pattern}
|
||||
}
|
||||
|
||||
func (lr *LazyRegexp) Get() (*regexp.Regexp, error) {
|
||||
lr.once.Do(func() {
|
||||
lr.re, lr.err = regexp.Compile(lr.pattern)
|
||||
})
|
||||
return lr.re, lr.err
|
||||
}
|
||||
|
||||
type PatternValue struct {
|
||||
Name string
|
||||
Value uint8
|
||||
}
|
||||
|
||||
type RateLimit struct {
|
||||
Count uint32
|
||||
Period time.Duration
|
||||
}
|
||||
|
||||
type AlertGroup struct {
|
||||
Name string
|
||||
Message string
|
||||
RateLimits []RateLimit
|
||||
RateLimitResetPeriod time.Duration
|
||||
}
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
package config
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
func NewLoginSSH(isNotify bool) []*Source {
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
|
||||
)
|
||||
|
||||
func NewLoginSSH(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSystemdUnit, "ssh.service")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for SSH login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: &SourceJournal{
|
||||
Field: JournalFieldSystemdUnit,
|
||||
Match: "ssh.service",
|
||||
},
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-ssh",
|
||||
Message: i18n.Lang.T("alert.login.ssh.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: NewLazyRegexp(`^Accepted (\S+) for (\S+) from (\S+) port \S+`),
|
||||
Regexp: regular_expression.NewLazyRegexp(`^Accepted (\S+) for (\S+) from (\S+) port \S+`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
@@ -36,26 +43,27 @@ func NewLoginSSH(isNotify bool) []*Source {
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func NewLoginLocal(isNotify bool) []*Source {
|
||||
func NewLoginLocal(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: &SourceJournal{
|
||||
Field: JournalFieldSyslogIdentifier,
|
||||
Match: "login",
|
||||
},
|
||||
journal, err := NewSourceJournal(JournalFieldSyslogIdentifier, "login")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for local login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-local",
|
||||
Message: i18n.Lang.T("alert.login.local.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: NewLazyRegexp(`^pam_unix\(login:session\): session opened for user (\S+)\(\S+\) by \S+`),
|
||||
Regexp: regular_expression.NewLazyRegexp(`^pam_unix\(login:session\): session opened for user (\S+)\(\S+\) by \S+`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
@@ -70,25 +78,27 @@ func NewLoginLocal(isNotify bool) []*Source {
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func NewLoginSu(isNotify bool) []*Source {
|
||||
func NewLoginSu(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSyslogIdentifier, "su")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for su login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: &SourceJournal{
|
||||
Field: JournalFieldSyslogIdentifier,
|
||||
Match: "su",
|
||||
},
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-su",
|
||||
Message: i18n.Lang.T("alert.login.su.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: NewLazyRegexp(`^pam_unix\(su:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`),
|
||||
Regexp: regular_expression.NewLazyRegexp(`^pam_unix\(su:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
@@ -107,25 +117,27 @@ func NewLoginSu(isNotify bool) []*Source {
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func NewLoginSudo(isNotify bool) []*Source {
|
||||
func NewLoginSudo(isNotify bool) ([]*Source, error) {
|
||||
var sources []*Source
|
||||
|
||||
journal, err := NewSourceJournal(JournalFieldSyslogIdentifier, "sudo")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create journal source for sudo login: %w", err)
|
||||
}
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
Journal: &SourceJournal{
|
||||
Field: JournalFieldSyslogIdentifier,
|
||||
Match: "sudo",
|
||||
},
|
||||
Type: SourceTypeJournal,
|
||||
Journal: journal,
|
||||
AlertRule: &AlertRule{
|
||||
Name: "_login-sudo",
|
||||
Message: i18n.Lang.T("alert.login.sudo.message"),
|
||||
IsNotification: isNotify,
|
||||
Patterns: []AlertRegexPattern{
|
||||
{
|
||||
Regexp: NewLazyRegexp(`^pam_unix\(sudo:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`),
|
||||
Regexp: regular_expression.NewLazyRegexp(`^pam_unix\(sudo:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`),
|
||||
Values: []PatternValue{
|
||||
{
|
||||
Name: i18n.Lang.T("user"),
|
||||
@@ -144,5 +156,5 @@ func NewLoginSudo(isNotify bool) []*Source {
|
||||
|
||||
sources = append(sources, source)
|
||||
|
||||
return sources
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
@@ -1,25 +1,57 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
"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/notifications"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type Analysis interface {
|
||||
Alert(entry *analysisServices.Entry)
|
||||
BruteForceProtection(entry *analysisServices.Entry)
|
||||
ClearDBData() ([]error, error)
|
||||
}
|
||||
|
||||
type analysis struct {
|
||||
alertService analysisServices.Alert
|
||||
alertService analysisServices.Alert
|
||||
bruteForceProtectionService analysisServices.BruteForceProtection
|
||||
}
|
||||
|
||||
func NewAnalysis(alertRuleIndex analysisServices.AlertRuleIndex, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||
func NewAnalysis(rulesIndex *analysisServices.RulesIndex, blockService brute_force_protection_group.BlockService, repositories db.Repositories, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||
alertGroupService := alert_group.NewGroup(repositories.AlertGroup(), logger)
|
||||
bruteForceProtectionGroupService := brute_force_protection_group.NewGroup(repositories.BruteForceProtectionGroup(), logger)
|
||||
|
||||
return &analysis{
|
||||
alertService: analysisServices.NewAlert(alertRuleIndex, logger, notify),
|
||||
alertService: analysisServices.NewAlert(rulesIndex, alertGroupService, logger, notify),
|
||||
bruteForceProtectionService: analysisServices.NewBruteForceProtection(rulesIndex, bruteForceProtectionGroupService, blockService, logger, notify),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analysis) Alert(entry *analysisServices.Entry) {
|
||||
a.alertService.Analyze(entry)
|
||||
}
|
||||
|
||||
func (a *analysis) BruteForceProtection(entry *analysisServices.Entry) {
|
||||
a.bruteForceProtectionService.Analyze(entry)
|
||||
}
|
||||
|
||||
func (a *analysis) ClearDBData() ([]error, error) {
|
||||
var errClearDB []error
|
||||
if err := a.alertService.ClearDBData(); err != nil {
|
||||
errClearDB = append(errClearDB, err)
|
||||
}
|
||||
if err := a.bruteForceProtectionService.ClearDBData(); err != nil {
|
||||
errClearDB = append(errClearDB, err)
|
||||
}
|
||||
|
||||
if len(errClearDB) > 0 {
|
||||
return nil, fmt.Errorf("failed to clear database data: %v", errClearDB)
|
||||
}
|
||||
|
||||
return errClearDB, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/analyzer/log/analysis/alert_group"
|
||||
"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"
|
||||
@@ -12,12 +13,14 @@ import (
|
||||
|
||||
type Alert interface {
|
||||
Analyze(entry *Entry)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type alert struct {
|
||||
ruleIndex AlertRuleIndex
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
rulesIndex *RulesIndex
|
||||
alertGroupService alert_group.Group
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
}
|
||||
|
||||
type alertAnalyzeRuleReturn struct {
|
||||
@@ -32,16 +35,17 @@ type alertNotify struct {
|
||||
fields []*regexField
|
||||
}
|
||||
|
||||
func NewAlert(ruleIndex AlertRuleIndex, logger log.Logger, notify notifications.Notifications) Alert {
|
||||
func NewAlert(rulesIndex *RulesIndex, alertGroupService alert_group.Group, logger log.Logger, notify notifications.Notifications) Alert {
|
||||
return &alert{
|
||||
ruleIndex: ruleIndex,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
rulesIndex: rulesIndex,
|
||||
alertGroupService: alertGroupService,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *alert) Analyze(entry *Entry) {
|
||||
rules, err := a.ruleIndex.Rules(entry)
|
||||
rules, err := a.rulesIndex.Alerts(entry)
|
||||
if err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to get alert rules: %s", err))
|
||||
}
|
||||
@@ -53,7 +57,19 @@ func (a *alert) Analyze(entry *Entry) {
|
||||
groupName := ""
|
||||
messages := []string{}
|
||||
if rule.Group != nil {
|
||||
alertGroup, err := a.alertGroupService.Analyze(rule.Group, entry.Time, entry.Message)
|
||||
if err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to analyze alert group: %s", err))
|
||||
continue
|
||||
}
|
||||
if !alertGroup.Alerted {
|
||||
continue
|
||||
}
|
||||
|
||||
groupName = rule.Group.Name
|
||||
for _, lastLog := range alertGroup.LastLogs {
|
||||
messages = append(messages, lastLog)
|
||||
}
|
||||
} else {
|
||||
messages = append(messages, entry.Message)
|
||||
}
|
||||
@@ -67,6 +83,10 @@ func (a *alert) Analyze(entry *Entry) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *alert) ClearDBData() error {
|
||||
return a.alertGroupService.ClearDBData()
|
||||
}
|
||||
|
||||
func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyzeRuleReturn {
|
||||
result := alertAnalyzeRuleReturn{
|
||||
found: false,
|
||||
@@ -86,8 +106,8 @@ func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyze
|
||||
for _, value := range pattern.Values {
|
||||
start, end, err := getValueStartEndByRegexIndex(int(value.Value), idx)
|
||||
if err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to get value start/end: %s", err))
|
||||
break
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: i18n.Lang.T("unknown")})
|
||||
continue
|
||||
}
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: message[start:end]})
|
||||
}
|
||||
|
||||
108
internal/daemon/analyzer/log/analysis/alert_group/group.go
Normal file
108
internal/daemon/analyzer/log/analysis/alert_group/group.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package alert_group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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/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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/time_operation"
|
||||
)
|
||||
|
||||
type Group interface {
|
||||
Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message string) (AnalysisResult, error)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type group struct {
|
||||
alertGroupRepository repository.AlertGroupRepository
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Alerted bool
|
||||
LastLogs []string
|
||||
}
|
||||
|
||||
func NewGroup(alertGroupRepository repository.AlertGroupRepository, logger log.Logger) Group {
|
||||
return &group{
|
||||
alertGroupRepository: alertGroupRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *group) Analyze(alertGroup *config.AlertGroup, eventTime time.Time, message 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) {
|
||||
rateLimit, err := alertGroup.RateLimit(entityAlertGroup.CurrentLevelTriggerCount)
|
||||
if err != nil {
|
||||
return entityAlertGroup, err
|
||||
}
|
||||
|
||||
if time_operation.IsRateLimited(entityAlertGroup.LastTriggeredAtUnix, eventTime, int64(rateLimit.Period)) {
|
||||
g.logger.Debug(fmt.Sprintf("Alert group %s is rate limited", alertGroup.Name))
|
||||
analysisResult, entityAlertGroup = g.analysisResult(rateLimit, eventTime, message, entityAlertGroup)
|
||||
return entityAlertGroup, nil
|
||||
}
|
||||
|
||||
entityAlertGroup.TriggerCount = 0
|
||||
|
||||
if time_operation.IsReset(entityAlertGroup.LastTriggeredAtUnix, eventTime, int64(alertGroup.RateLimitResetPeriod)) {
|
||||
g.logger.Debug(fmt.Sprintf("Alert group %s is reset", alertGroup.Name))
|
||||
entityAlertGroup.Reset()
|
||||
rateLimit, err = alertGroup.RateLimit(0)
|
||||
if err != nil {
|
||||
return entityAlertGroup, err
|
||||
}
|
||||
}
|
||||
|
||||
g.logger.Debug(fmt.Sprintf("Alert not rate limited"))
|
||||
analysisResult, entityAlertGroup = g.analysisResult(rateLimit, eventTime, message, entityAlertGroup)
|
||||
|
||||
return entityAlertGroup, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return AnalysisResult{
|
||||
Alerted: false,
|
||||
}, err
|
||||
}
|
||||
|
||||
return analysisResult, nil
|
||||
}
|
||||
|
||||
func (g *group) ClearDBData() error {
|
||||
return g.alertGroupRepository.Clear()
|
||||
}
|
||||
|
||||
func (g *group) analysisResult(rateLimit config.RateLimit, eventTime time.Time, message string, entityAlertGroup *entity.AlertGroup) (AnalysisResult, *entity.AlertGroup) {
|
||||
analysisResult := AnalysisResult{
|
||||
Alerted: false,
|
||||
}
|
||||
|
||||
entityAlertGroup.LastTriggeredAtUnix = eventTime.Unix()
|
||||
entityAlertGroup.TriggerCount++
|
||||
entityAlertGroup.LastLogs = append(entityAlertGroup.LastLogs, fmt.Sprintf("event time: %s, message: %s", eventTime.Format(time.RFC3339), message))
|
||||
g.logger.Debug(fmt.Sprintf("Alert triggered. Count: %d", entityAlertGroup.TriggerCount))
|
||||
|
||||
if entityAlertGroup.TriggerCount >= uint64(rateLimit.Count) {
|
||||
g.logger.Debug(fmt.Sprintf("Alert reached rate limit"))
|
||||
analysisResult.LastLogs = entityAlertGroup.LastLogs
|
||||
analysisResult.Alerted = true
|
||||
|
||||
entityAlertGroup.CurrentLevelTriggerCount++
|
||||
entityAlertGroup.TriggerCount = 0
|
||||
entityAlertGroup.LastLogs = []string{}
|
||||
} else {
|
||||
g.logger.Debug(fmt.Sprintf("Alert not reached rate limit"))
|
||||
}
|
||||
|
||||
return analysisResult, entityAlertGroup
|
||||
}
|
||||
@@ -8,12 +8,15 @@ import (
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
Source config.SourceType
|
||||
Message string
|
||||
Unit string
|
||||
PID string
|
||||
SyslogIdentifier string
|
||||
Time time.Time
|
||||
Source config.SourceType
|
||||
Message string
|
||||
Time time.Time
|
||||
|
||||
Unit string // for systemd source
|
||||
PID string // for systemd source
|
||||
SyslogIdentifier string // for systemd source
|
||||
|
||||
File string // for file source
|
||||
}
|
||||
|
||||
type regexField struct {
|
||||
|
||||
300
internal/daemon/analyzer/log/analysis/brute_force_protection.go
Normal file
300
internal/daemon/analyzer/log/analysis/brute_force_protection.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"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/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"
|
||||
"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 BruteForceProtection interface {
|
||||
Analyze(entry *Entry)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type bruteForceProtection struct {
|
||||
rulesIndex *RulesIndex
|
||||
groupService brute_force_protection_group.Group
|
||||
blockService brute_force_protection_group.BlockService
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
}
|
||||
|
||||
type bruteForceProtectionAnalyzeRuleReturn struct {
|
||||
found bool
|
||||
fields []*regexField
|
||||
ip net.IP
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func NewBruteForceProtection(rulesIndex *RulesIndex, groupService brute_force_protection_group.Group, blockService brute_force_protection_group.BlockService, logger log.Logger, notify notifications.Notifications) BruteForceProtection {
|
||||
return &bruteForceProtection{
|
||||
rulesIndex: rulesIndex,
|
||||
groupService: groupService,
|
||||
blockService: blockService,
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) Analyze(entry *Entry) {
|
||||
rules, err := p.rulesIndex.BruteForceProtections(entry)
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to get brute force protection rules for entry: %v", err))
|
||||
return
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if rule.Group == nil {
|
||||
p.logger.Error("Brute force protection rule without group")
|
||||
continue
|
||||
}
|
||||
|
||||
result := p.analyzeRule(rule, entry.Message)
|
||||
if !result.found {
|
||||
continue
|
||||
}
|
||||
|
||||
groupResult, err := p.groupService.Analyze(rule.Group, entry.Time, result.ip, entry.Message)
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to analyze brute force protection group: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if !groupResult.Block {
|
||||
continue
|
||||
}
|
||||
|
||||
ipWithPorts, l4Ports := groupResult.BlockConfig.PortsBlocked()
|
||||
if !ipWithPorts {
|
||||
p.handleBlockIP(entry, rule, &result, &groupResult)
|
||||
continue
|
||||
}
|
||||
|
||||
p.handleBlockIPWithPorts(entry, rule, &result, &groupResult, l4Ports)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) ClearDBData() error {
|
||||
return p.groupService.ClearDBData()
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) handleBlockIP(
|
||||
entry *Entry,
|
||||
rule *brute_force_protection.Rule,
|
||||
result *bruteForceProtectionAnalyzeRuleReturn,
|
||||
groupResult *brute_force_protection_group.AnalysisResult,
|
||||
) {
|
||||
blockIP := blocking.BlockIP{
|
||||
IP: result.ip,
|
||||
TimeSeconds: groupResult.BlockSec,
|
||||
Reason: rule.Message,
|
||||
}
|
||||
isBanned, err := p.blockService.BlockIP(blockIP)
|
||||
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,
|
||||
})
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) handleBlockIPWithPorts(
|
||||
entry *Entry,
|
||||
rule *brute_force_protection.Rule,
|
||||
result *bruteForceProtectionAnalyzeRuleReturn,
|
||||
groupResult *brute_force_protection_group.AnalysisResult,
|
||||
l4Ports []types.L4Port,
|
||||
) {
|
||||
blockIPWithPorts := blocking.BlockIPWithPorts{
|
||||
IP: result.ip,
|
||||
TimeSeconds: groupResult.BlockSec,
|
||||
Reason: rule.Message,
|
||||
Ports: l4Ports,
|
||||
}
|
||||
isBanned, err := p.blockService.BlockIPWithPorts(blockIPWithPorts)
|
||||
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,
|
||||
})
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, message string) bruteForceProtectionAnalyzeRuleReturn {
|
||||
result := bruteForceProtectionAnalyzeRuleReturn{
|
||||
found: false,
|
||||
fields: []*regexField{},
|
||||
ip: nil,
|
||||
}
|
||||
|
||||
for _, pattern := range rule.Patterns {
|
||||
re, err := pattern.Regexp.Get()
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to compile regexp: %s", err))
|
||||
continue
|
||||
}
|
||||
|
||||
idx := re.FindStringSubmatchIndex(message)
|
||||
|
||||
if idx != nil {
|
||||
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
|
||||
}
|
||||
ipText := message[start:end]
|
||||
result.ip = net.ParseIP(ipText)
|
||||
if result.ip == nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to parse ip: %s", ipText))
|
||||
return bruteForceProtectionAnalyzeRuleReturn{
|
||||
found: false,
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range pattern.Values {
|
||||
start, end, err := getValueStartEndByRegexIndex(int(value.Value), idx)
|
||||
if err != nil {
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: i18n.Lang.T("unknown")})
|
||||
continue
|
||||
}
|
||||
result.fields = append(result.fields, ®exField{name: value.Name, value: message[start:end]})
|
||||
}
|
||||
|
||||
if len(pattern.Values) != len(result.fields) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.found = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotifySuccess(notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := notify.rule.Group.Name
|
||||
|
||||
subject := i18n.Lang.T("alert.bruteForceProtection.subject", map[string]any{
|
||||
"Name": notify.rule.Name,
|
||||
"GroupName": groupName,
|
||||
"IP": notify.ip,
|
||||
})
|
||||
|
||||
p.sendNotify(subject, notify)
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotifyError(notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupName := notify.rule.Group.Name
|
||||
|
||||
subject := i18n.Lang.T("alert.bruteForceProtection.subject-error", map[string]any{
|
||||
"Name": notify.rule.Name,
|
||||
"GroupName": groupName,
|
||||
"IP": notify.ip,
|
||||
})
|
||||
|
||||
p.sendNotify(subject, notify)
|
||||
}
|
||||
|
||||
func (p *bruteForceProtection) sendNotify(subject string, notify *bruteForceProtectionNotify) {
|
||||
if !notify.rule.IsNotification {
|
||||
return
|
||||
}
|
||||
|
||||
groupMessage := notify.rule.Group.Message + "\n\n"
|
||||
|
||||
text := subject + "\n\n" + groupMessage + notify.rule.Message + "\n\n"
|
||||
if notify.err != nil {
|
||||
text += i18n.Lang.T("alert.bruteForceProtection.error", map[string]any{
|
||||
"Error": notify.err.Error(),
|
||||
}) + "\n"
|
||||
}
|
||||
text += "IP: " + notify.ip.String() + "\n"
|
||||
if len(notify.ports) > 0 {
|
||||
var ports []string
|
||||
for _, port := range notify.ports {
|
||||
ports = append(ports, port.ToString())
|
||||
}
|
||||
text += i18n.Lang.T("ports", map[string]any{
|
||||
"Ports": strings.Join(ports, ", "),
|
||||
}) + "\n"
|
||||
}
|
||||
text += i18n.Lang.T("blockSec", map[string]any{
|
||||
"BlockSec": notify.blockSec,
|
||||
}) + "\n"
|
||||
text += i18n.Lang.T("time", map[string]any{
|
||||
"Time": notify.time,
|
||||
}) + "\n"
|
||||
for _, field := range notify.fields {
|
||||
text += fmt.Sprintf("%s: %s\n", field.name, field.value)
|
||||
}
|
||||
text += "\n" + i18n.Lang.T("log") + "\n"
|
||||
for _, message := range notify.messages {
|
||||
text += message + "\n"
|
||||
}
|
||||
p.notify.SendAsync(notifications.Message{Subject: subject, Body: text})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package brute_force_protection_group
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
|
||||
|
||||
type BlockService interface {
|
||||
BlockIP(blockIP blocking.BlockIP) (bool, error)
|
||||
BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error)
|
||||
}
|
||||
|
||||
type BlockIPFunc func(blockIP blocking.BlockIP) (bool, error)
|
||||
type BlockIPWithPortsFunc func(blockIP blocking.BlockIPWithPorts) (bool, error)
|
||||
|
||||
type blockService struct {
|
||||
blockIPFunc BlockIPFunc
|
||||
blockIPWithPortsFunc BlockIPWithPortsFunc
|
||||
}
|
||||
|
||||
func NewBlockService(blockIPFunc BlockIPFunc, blockIPWithPortsFunc BlockIPWithPortsFunc) BlockService {
|
||||
return &blockService{
|
||||
blockIPFunc: blockIPFunc,
|
||||
blockIPWithPortsFunc: blockIPWithPortsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blockService) BlockIP(blockIP blocking.BlockIP) (bool, error) {
|
||||
return b.blockIPFunc(blockIP)
|
||||
}
|
||||
|
||||
func (b *blockService) BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error) {
|
||||
return b.blockIPWithPortsFunc(blockIP)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package brute_force_protection_group
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/time_operation"
|
||||
)
|
||||
|
||||
type Group interface {
|
||||
Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message string) (AnalysisResult, error)
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type group struct {
|
||||
groupRepository repository.BruteForceProtectionGroupRepository
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Block bool
|
||||
BlockSec uint32
|
||||
BlockConfig brute_force_protection.Block
|
||||
LastLogs []string
|
||||
}
|
||||
|
||||
func NewGroup(groupRepository repository.BruteForceProtectionGroupRepository, logger log.Logger) Group {
|
||||
return &group{
|
||||
groupRepository: groupRepository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *group) Analyze(group *brute_force_protection.Group, eventTime time.Time, ip net.IP, message 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) {
|
||||
rateLimit, err := group.RateLimit(entityGroup.CurrentLevelTriggerCount)
|
||||
if err != nil {
|
||||
return entityGroup, err
|
||||
}
|
||||
|
||||
if time_operation.IsRateLimited(entityGroup.LastTriggeredAtUnix, eventTime, int64(rateLimit.Period)) {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection group %s is rate limited", group.Name))
|
||||
analysisResult, entityGroup = g.analysisResult(rateLimit, eventTime, message, entityGroup)
|
||||
return entityGroup, nil
|
||||
}
|
||||
|
||||
entityGroup.TriggerCount = 0
|
||||
|
||||
if time_operation.IsReset(entityGroup.LastTriggeredAtUnix, eventTime, int64(group.RateLimitResetPeriod)) {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection group %s is reset", group.Name))
|
||||
entityGroup.Reset()
|
||||
rateLimit, err = group.RateLimit(0)
|
||||
if err != nil {
|
||||
return entityGroup, err
|
||||
}
|
||||
}
|
||||
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection not rate limited"))
|
||||
analysisResult, entityGroup = g.analysisResult(rateLimit, eventTime, message, entityGroup)
|
||||
|
||||
return entityGroup, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return AnalysisResult{
|
||||
Block: false,
|
||||
}, err
|
||||
}
|
||||
|
||||
return analysisResult, nil
|
||||
}
|
||||
|
||||
func (g *group) ClearDBData() error {
|
||||
return g.groupRepository.Clear()
|
||||
}
|
||||
|
||||
func (g *group) analysisResult(rateLimit brute_force_protection.RateLimit, eventTime time.Time, message string, entityGroup *entity.BruteForceProtectionGroup) (AnalysisResult, *entity.BruteForceProtectionGroup) {
|
||||
analysisResult := AnalysisResult{
|
||||
Block: false,
|
||||
}
|
||||
|
||||
entityGroup.LastTriggeredAtUnix = eventTime.Unix()
|
||||
entityGroup.TriggerCount++
|
||||
entityGroup.LastLogs = append(entityGroup.LastLogs, fmt.Sprintf("event time: %s, message: %s", eventTime.Format(time.RFC3339), message))
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection triggered. Count: %d", entityGroup.TriggerCount))
|
||||
|
||||
if entityGroup.TriggerCount >= uint64(rateLimit.Count) {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection reached rate limit"))
|
||||
analysisResult.LastLogs = entityGroup.LastLogs
|
||||
analysisResult.Block = true
|
||||
analysisResult.BlockSec = rateLimit.BlockingTimeSeconds
|
||||
analysisResult.BlockConfig = rateLimit.BlockConfig
|
||||
|
||||
entityGroup.CurrentLevelTriggerCount++
|
||||
entityGroup.TriggerCount = 0
|
||||
entityGroup.LastLogs = []string{}
|
||||
} else {
|
||||
g.logger.Debug(fmt.Sprintf("Brute force protection not reached rate limit"))
|
||||
}
|
||||
|
||||
return analysisResult, entityGroup
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
config2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
type AlertRuleIndex struct {
|
||||
byKey map[string][]*config2.AlertRule
|
||||
}
|
||||
|
||||
func (idx *AlertRuleIndex) Add(source *config2.Source) error {
|
||||
key, err := generateIndexKeyBySource(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx.byKey[key] = append(idx.byKey[key], source.AlertRule)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *AlertRuleIndex) Rules(entry *Entry) ([]*config2.AlertRule, error) {
|
||||
var rules []*config2.AlertRule
|
||||
|
||||
keys, err := generateIndexKeysByEntry(entry)
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
rules = append(rules, idx.byKey[key]...)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func NewAlertRuleIndex() AlertRuleIndex {
|
||||
return AlertRuleIndex{byKey: make(map[string][]*config2.AlertRule)}
|
||||
}
|
||||
|
||||
func generateIndexKeyBySource(source *config2.Source) (string, error) {
|
||||
switch source.Type {
|
||||
case config2.SourceTypeJournal:
|
||||
match := source.Journal.JournalctlMatch()
|
||||
if source.Journal.Field == "" || source.Journal.Match == "" {
|
||||
return "", errors.New("journalctl match is empty")
|
||||
}
|
||||
return string(source.Type) + ":" + match, nil
|
||||
}
|
||||
|
||||
return "", errors.New(fmt.Sprintf("unknown source type: %s", source.Type))
|
||||
}
|
||||
|
||||
func generateIndexKeysByEntry(entry *Entry) ([]string, error) {
|
||||
var keys []string
|
||||
|
||||
switch entry.Source {
|
||||
case config2.SourceTypeJournal:
|
||||
source := string(entry.Source) + ":"
|
||||
|
||||
keys = append(keys, source+string(config2.JournalFieldSystemdUnit)+"="+entry.Unit)
|
||||
keys = append(keys, source+string(config2.JournalFieldSyslogIdentifier)+"="+entry.SyslogIdentifier)
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
return []string{}, errors.New(fmt.Sprintf("unknown source type: %s", entry.Source))
|
||||
}
|
||||
41
internal/daemon/analyzer/log/analysis/rules_bucket.go
Normal file
41
internal/daemon/analyzer/log/analysis/rules_bucket.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
config2 "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/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
type RulesBucket interface {
|
||||
Alerts() []*config2.AlertRule
|
||||
BruteForceProtectionRules() []*brute_force_protection.Rule
|
||||
|
||||
addAlertRule(rule *config2.AlertRule)
|
||||
addBruteForceProtectionRule(rule *brute_force_protection.Rule)
|
||||
}
|
||||
|
||||
type rulesBucket struct {
|
||||
alerts []*config2.AlertRule
|
||||
bruteForceProtectionRules []*brute_force_protection.Rule
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) Alerts() []*config2.AlertRule {
|
||||
return rb.alerts
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) BruteForceProtectionRules() []*brute_force_protection.Rule {
|
||||
return rb.bruteForceProtectionRules
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) addAlertRule(rule *config2.AlertRule) {
|
||||
rb.alerts = append(rb.alerts, rule)
|
||||
}
|
||||
|
||||
func (rb *rulesBucket) addBruteForceProtectionRule(rule *brute_force_protection.Rule) {
|
||||
rb.bruteForceProtectionRules = append(rb.bruteForceProtectionRules, rule)
|
||||
}
|
||||
|
||||
func newRulesBucket() RulesBucket {
|
||||
return &rulesBucket{
|
||||
alerts: make([]*config2.AlertRule, 0),
|
||||
}
|
||||
}
|
||||
118
internal/daemon/analyzer/log/analysis/rules_index.go
Normal file
118
internal/daemon/analyzer/log/analysis/rules_index.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
config2 "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/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
type RulesIndex struct {
|
||||
byKey map[indexKey]RulesBucket
|
||||
}
|
||||
|
||||
type indexKey struct {
|
||||
source config2.SourceType
|
||||
val string
|
||||
}
|
||||
|
||||
func (idx *RulesIndex) Add(source *config2.Source) error {
|
||||
if source.AlertRule == nil && source.BruteForceProtectionRule == nil {
|
||||
return fmt.Errorf("no alert rule or brute force protection rule")
|
||||
}
|
||||
|
||||
key, err := generateIndexKeyBySource(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := idx.byKey[key]; !ok {
|
||||
idx.byKey[key] = newRulesBucket()
|
||||
}
|
||||
|
||||
if source.AlertRule != nil {
|
||||
idx.byKey[key].addAlertRule(source.AlertRule)
|
||||
}
|
||||
|
||||
if source.BruteForceProtectionRule != nil {
|
||||
idx.byKey[key].addBruteForceProtectionRule(source.BruteForceProtectionRule)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *RulesIndex) Alerts(entry *Entry) ([]*config2.AlertRule, error) {
|
||||
rules := make([]*config2.AlertRule, 0)
|
||||
|
||||
keys, err := generateIndexKeysByEntry(entry)
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
b, ok := idx.byKey[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, b.Alerts()...)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (idx *RulesIndex) BruteForceProtections(entry *Entry) ([]*brute_force_protection.Rule, error) {
|
||||
rules := make([]*brute_force_protection.Rule, 0)
|
||||
|
||||
keys, err := generateIndexKeysByEntry(entry)
|
||||
if err != nil {
|
||||
return rules, err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
b, ok := idx.byKey[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, b.BruteForceProtectionRules()...)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func NewRulesIndex() *RulesIndex {
|
||||
return &RulesIndex{byKey: make(map[indexKey]RulesBucket)}
|
||||
}
|
||||
|
||||
func generateIndexKeyBySource(source *config2.Source) (indexKey, error) {
|
||||
switch source.Type {
|
||||
case config2.SourceTypeJournal:
|
||||
match := source.Journal.JournalctlMatch()
|
||||
if source.Journal.Field == "" || source.Journal.Match == "" {
|
||||
return indexKey{}, errors.New("journalctl match is empty")
|
||||
}
|
||||
return indexKey{source: source.Type, val: match}, nil
|
||||
case config2.SourceTypeFile:
|
||||
return indexKey{source: source.Type, val: source.File.Path}, nil
|
||||
}
|
||||
|
||||
return indexKey{}, errors.New(fmt.Sprintf("unknown source type: %s", source.Type))
|
||||
}
|
||||
|
||||
func generateIndexKeysByEntry(entry *Entry) ([]indexKey, error) {
|
||||
var keys []indexKey
|
||||
|
||||
switch entry.Source {
|
||||
case config2.SourceTypeJournal:
|
||||
keys = append(keys, indexKey{source: entry.Source, val: string(config2.JournalFieldSystemdUnit) + "=" + entry.Unit})
|
||||
keys = append(keys, indexKey{source: entry.Source, val: string(config2.JournalFieldSyslogIdentifier) + "=" + entry.SyslogIdentifier})
|
||||
return keys, nil
|
||||
case config2.SourceTypeFile:
|
||||
keys = append(keys, indexKey{source: entry.Source, val: entry.File})
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
return []indexKey{}, errors.New(fmt.Sprintf("unknown source type: %s", entry.Source))
|
||||
}
|
||||
111
internal/daemon/analyzer/log/file_monitoring.go
Normal file
111
internal/daemon/analyzer/log/file_monitoring.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
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/file_monitoring"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"github.com/nxadm/tail"
|
||||
)
|
||||
|
||||
type FileMonitoring interface {
|
||||
Run(ctx context.Context, logChan chan<- analysisServices.Entry)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type fileMonitoring struct {
|
||||
paths []string
|
||||
logger log.Logger
|
||||
|
||||
tailers []*tail.Tail
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewFileMonitoring(paths []string, logger log.Logger) FileMonitoring {
|
||||
return &fileMonitoring{
|
||||
paths: paths,
|
||||
logger: logger,
|
||||
tailers: []*tail.Tail{},
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *fileMonitoring) Run(ctx context.Context, logChan chan<- analysisServices.Entry) {
|
||||
pathsCount := len(fm.paths)
|
||||
if pathsCount == 0 {
|
||||
fm.logger.Debug("No paths specified for file monitoring")
|
||||
return
|
||||
}
|
||||
|
||||
fm.logger.Debug("Starting file monitoring")
|
||||
|
||||
tailLogger := file_monitoring.NewLogger(fm.logger)
|
||||
|
||||
for _, path := range fm.paths {
|
||||
path := path
|
||||
go func() {
|
||||
fm.monitorFile(path, ctx, logChan, tailLogger)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *fileMonitoring) Close() error {
|
||||
for _, t := range fm.tailers {
|
||||
_ = t.Stop()
|
||||
fm.logger.Debug(fmt.Sprintf("Stopped monitoring file %s", t.Filename))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fm *fileMonitoring) monitorFile(path string, ctx context.Context, logChan chan<- analysisServices.Entry, tailLogger file_monitoring.Logger) {
|
||||
fm.logger.Debug(fmt.Sprintf("Monitoring file %s", path))
|
||||
t, err := tail.TailFile(path, tail.Config{
|
||||
Follow: true,
|
||||
ReOpen: true,
|
||||
Poll: true,
|
||||
Location: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},
|
||||
Logger: tailLogger,
|
||||
})
|
||||
|
||||
fm.mu.Lock()
|
||||
fm.tailers = append(fm.tailers, t)
|
||||
fm.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
fm.logger.Error(fmt.Sprintf("Failed to tail file %s: %s", path, err))
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case line, ok := <-t.Lines:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if line == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
entry := analysisServices.Entry{
|
||||
Source: config.SourceTypeFile,
|
||||
File: path,
|
||||
Message: line.Text,
|
||||
Time: time.Now(),
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case logChan <- entry:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
internal/daemon/analyzer/log/file_monitoring/logger.go
Normal file
63
internal/daemon/analyzer/log/file_monitoring/logger.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package file_monitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Fatal(v ...interface{})
|
||||
Fatalf(format string, v ...interface{})
|
||||
Fatalln(v ...interface{})
|
||||
Panic(v ...interface{})
|
||||
Panicf(format string, v ...interface{})
|
||||
Panicln(v ...interface{})
|
||||
Print(v ...interface{})
|
||||
Printf(format string, v ...interface{})
|
||||
Println(v ...interface{})
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewLogger(log log.Logger) Logger {
|
||||
return &logger{logger: log}
|
||||
}
|
||||
|
||||
func (l *logger) Fatal(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Fatalf(format string, v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: "+format, v...))
|
||||
}
|
||||
|
||||
func (l *logger) Fatalln(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Panic(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Panicf(format string, v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: "+format, v...))
|
||||
}
|
||||
|
||||
func (l *logger) Panicln(v ...interface{}) {
|
||||
l.logger.Error(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Print(v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
|
||||
func (l *logger) Printf(format string, v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: "+format, v...))
|
||||
}
|
||||
|
||||
func (l *logger) Println(v ...interface{}) {
|
||||
l.logger.Warn(fmt.Sprintf("File Monitoring: %v", v...))
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
||||
entryTime = time.Now()
|
||||
}
|
||||
|
||||
logChan <- analysisServices.Entry{
|
||||
entry := analysisServices.Entry{
|
||||
Source: config.SourceTypeJournal,
|
||||
Message: raw.Message,
|
||||
Unit: raw.Unit,
|
||||
@@ -127,6 +127,12 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
||||
SyslogIdentifier: raw.SyslogIdentifier,
|
||||
Time: entryTime,
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case logChan <- entry:
|
||||
}
|
||||
}
|
||||
|
||||
return cmd.Wait()
|
||||
|
||||
@@ -4,16 +4,22 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer"
|
||||
"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/types"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/notifications"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
type Daemon interface {
|
||||
@@ -106,10 +112,19 @@ func (d *daemon) runWorker(ctx context.Context, isTesting bool, testingInterval
|
||||
return
|
||||
case <-stopTestingCh:
|
||||
d.logger.Info("Testing interval expired, stopping service")
|
||||
err := d.notifications.DBQueueClear()
|
||||
if err != nil {
|
||||
|
||||
if err := d.notifications.DBQueueClear(); err != nil {
|
||||
d.logger.Error(fmt.Sprintf("failed to clear notifications queue: %v", err))
|
||||
}
|
||||
|
||||
if err := d.analyzer.ClearDBData(); err != nil {
|
||||
d.logger.Error(fmt.Sprintf("failed to clear analyzer data: %v", err))
|
||||
}
|
||||
|
||||
if err := d.firewall.ClearDBData(); err != nil {
|
||||
d.logger.Error(fmt.Sprintf("failed to clear firewall data: %v", err))
|
||||
}
|
||||
|
||||
d.Stop()
|
||||
return
|
||||
case <-d.stopCh:
|
||||
@@ -119,7 +134,7 @@ func (d *daemon) runWorker(ctx context.Context, isTesting bool, testingInterval
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) socketCommand(command string, socket socket.Connect) error {
|
||||
func (d *daemon) socketCommand(command string, args map[string]string, socket socket.Connect) error {
|
||||
switch command {
|
||||
case "stop":
|
||||
d.stopCh <- struct{}{}
|
||||
@@ -141,8 +156,140 @@ func (d *daemon) socketCommand(command string, socket socket.Connect) error {
|
||||
return err
|
||||
}
|
||||
return socket.Write("ok")
|
||||
case "block_add_ip":
|
||||
if args["ip"] == "" {
|
||||
return socket.Write("ip argument is required")
|
||||
}
|
||||
ipAddr := net.ParseIP(args["ip"])
|
||||
if ipAddr == nil {
|
||||
_ = socket.Write("invalid ip address")
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
port := args["port"]
|
||||
if port != "" {
|
||||
if err := d.cmdBlockAddIPWithPort(ipAddr, port, args); err != nil {
|
||||
_ = socket.Write("block add failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := d.cmdBlockAddIP(ipAddr, args); err != nil {
|
||||
_ = socket.Write("block add failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return socket.Write("ok")
|
||||
case "block_delete_ip":
|
||||
if args["ip"] == "" {
|
||||
return socket.Write("ip argument is required")
|
||||
}
|
||||
ipAddr := net.ParseIP(args["ip"])
|
||||
if ipAddr == nil {
|
||||
_ = socket.Write("invalid ip address")
|
||||
return errors.New("invalid ip address")
|
||||
}
|
||||
|
||||
if err := d.firewall.UnblockIP(ipAddr); err != nil {
|
||||
_ = socket.Write("block delete failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return socket.Write("ok")
|
||||
|
||||
case "block_clear":
|
||||
if err := d.firewall.UnblockAllIPs(); err != nil {
|
||||
_ = socket.Write("block clear failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return socket.Write("ok")
|
||||
default:
|
||||
_ = socket.Write("unknown command")
|
||||
return errors.New("unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *daemon) cmdBlockAddIP(ip net.IP, args map[string]string) error {
|
||||
blockIP := blocking.BlockIP{
|
||||
IP: ip,
|
||||
}
|
||||
|
||||
if args["seconds"] != "" {
|
||||
seconds, err := strconv.Atoi(args["seconds"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blockIP.TimeSeconds = uint32(seconds)
|
||||
}
|
||||
|
||||
if args["reason"] != "" {
|
||||
blockIP.Reason = args["reason"]
|
||||
}
|
||||
|
||||
isBlock, err := d.firewall.BlockIP(blockIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isBlock {
|
||||
return errors.New("the IP address is not blocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *daemon) cmdBlockAddIPWithPort(ip net.IP, port string, args map[string]string) error {
|
||||
l4Port, err := newL4PortFromString(port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blockIP := blocking.BlockIPWithPorts{
|
||||
IP: ip,
|
||||
Ports: []types.L4Port{l4Port},
|
||||
}
|
||||
|
||||
if args["seconds"] != "" {
|
||||
seconds, err := strconv.Atoi(args["seconds"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blockIP.TimeSeconds = uint32(seconds)
|
||||
}
|
||||
|
||||
if args["reason"] != "" {
|
||||
blockIP.Reason = args["reason"]
|
||||
}
|
||||
|
||||
isBlock, err := d.firewall.BlockIPWithPorts(blockIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isBlock {
|
||||
return errors.New("the IP address is not blocked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newL4PortFromString(s string) (types.L4Port, error) {
|
||||
if s == "" {
|
||||
return nil, errors.New("port is empty")
|
||||
}
|
||||
|
||||
data := strings.Split(s, "/")
|
||||
protocol := types.ProtocolTCP
|
||||
port, err := strconv.Atoi(data[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validate.Port(port, "port"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) == 2 {
|
||||
protocol, err = ip.ToProtocol(data[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return types.NewL4Port(uint16(port), protocol)
|
||||
}
|
||||
|
||||
@@ -10,17 +10,24 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
app = "app.db"
|
||||
appDB = "app.db"
|
||||
securityDB = "security.db"
|
||||
)
|
||||
|
||||
type Repositories interface {
|
||||
NotificationsQueue() repository.NotificationsQueueRepository
|
||||
AlertGroup() repository.AlertGroupRepository
|
||||
BruteForceProtectionGroup() repository.BruteForceProtectionGroupRepository
|
||||
Blocking() repository.BlockingRepository
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
type repositories struct {
|
||||
notificationsQueue repository.NotificationsQueueRepository
|
||||
notificationsQueue repository.NotificationsQueueRepository
|
||||
alertGroup repository.AlertGroupRepository
|
||||
bruteForceProtectionGroup repository.BruteForceProtectionGroupRepository
|
||||
blocking repository.BlockingRepository
|
||||
|
||||
db []*bbolt.DB
|
||||
}
|
||||
@@ -38,12 +45,20 @@ func New(dataDir string) (Repositories, error) {
|
||||
return &repositories{}, err
|
||||
}
|
||||
|
||||
appDB, err := bbolt.Open(dataDir+app, 0600, &bbolt.Options{Timeout: 3 * time.Second})
|
||||
appDB, err := bbolt.Open(dataDir+appDB, 0600, &bbolt.Options{Timeout: 3 * time.Second})
|
||||
if err != nil {
|
||||
return &repositories{}, err
|
||||
}
|
||||
|
||||
securityDB, err := bbolt.Open(dataDir+securityDB, 0600, &bbolt.Options{Timeout: 3 * time.Second})
|
||||
|
||||
return &repositories{
|
||||
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
|
||||
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
|
||||
alertGroup: repository.NewAlertGroupRepository(appDB),
|
||||
bruteForceProtectionGroup: repository.NewBruteForceProtectionGroupRepository(securityDB),
|
||||
blocking: repository.NewBlockingRepository(securityDB),
|
||||
|
||||
db: []*bbolt.DB{appDB},
|
||||
db: []*bbolt.DB{appDB, securityDB},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -51,6 +66,18 @@ func (r *repositories) NotificationsQueue() repository.NotificationsQueueReposit
|
||||
return r.notificationsQueue
|
||||
}
|
||||
|
||||
func (r *repositories) AlertGroup() repository.AlertGroupRepository {
|
||||
return r.alertGroup
|
||||
}
|
||||
|
||||
func (r *repositories) BruteForceProtectionGroup() repository.BruteForceProtectionGroupRepository {
|
||||
return r.bruteForceProtectionGroup
|
||||
}
|
||||
|
||||
func (r *repositories) Blocking() repository.BlockingRepository {
|
||||
return r.blocking
|
||||
}
|
||||
|
||||
func (r *repositories) Close() error {
|
||||
for _, db := range r.db {
|
||||
_ = db.Close()
|
||||
|
||||
15
internal/daemon/db/entity/alert_group.go
Normal file
15
internal/daemon/db/entity/alert_group.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package entity
|
||||
|
||||
type AlertGroup struct {
|
||||
LastTriggeredAtUnix int64 `json:"LastTriggeredAtUnix"`
|
||||
TriggerCount uint64 `json:"TriggerCount"`
|
||||
CurrentLevelTriggerCount uint64 `json:"CurrentLevelTriggerCount"`
|
||||
LastLogs []string `json:"LastLogs"`
|
||||
}
|
||||
|
||||
func (g *AlertGroup) Reset() {
|
||||
g.LastTriggeredAtUnix = 0
|
||||
g.TriggerCount = 0
|
||||
g.CurrentLevelTriggerCount = 0
|
||||
g.LastLogs = []string{}
|
||||
}
|
||||
55
internal/daemon/db/entity/blocking.go
Normal file
55
internal/daemon/db/entity/blocking.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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/pkg/ip"
|
||||
)
|
||||
|
||||
type Blocking struct {
|
||||
IP string `json:"IP"`
|
||||
Ports []BlockingPort
|
||||
ExpireAtUnix int64 `json:"ExpireAtUnix"`
|
||||
Reason string `json:"Reason"`
|
||||
}
|
||||
|
||||
func (b *Blocking) IsPorts() bool {
|
||||
return len(b.Ports) > 0
|
||||
}
|
||||
|
||||
func (b *Blocking) ToL4Ports() ([]types.L4Port, error) {
|
||||
if !b.IsPorts() {
|
||||
return nil, fmt.Errorf("ports is empty")
|
||||
}
|
||||
|
||||
l4Ports := make([]types.L4Port, 0, len(b.Ports))
|
||||
for _, port := range b.Ports {
|
||||
l4port, err := port.ToL4Port()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l4Ports = append(l4Ports, l4port)
|
||||
}
|
||||
|
||||
return l4Ports, nil
|
||||
}
|
||||
|
||||
type BlockingPort struct {
|
||||
Number uint16 `json:"Port"`
|
||||
Protocol string `json:"Protocol"`
|
||||
}
|
||||
|
||||
func (p *BlockingPort) ToL4Port() (types.L4Port, error) {
|
||||
if p.Protocol == "" {
|
||||
return nil, errors.New("protocol is empty")
|
||||
}
|
||||
|
||||
protocol, err := ip.ToProtocol(p.Protocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewL4Port(p.Number, protocol)
|
||||
}
|
||||
15
internal/daemon/db/entity/brute_force_protection_group.go
Normal file
15
internal/daemon/db/entity/brute_force_protection_group.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package entity
|
||||
|
||||
type BruteForceProtectionGroup struct {
|
||||
LastTriggeredAtUnix int64 `json:"LastTriggeredAtUnix"`
|
||||
TriggerCount uint64 `json:"TriggerCount"`
|
||||
CurrentLevelTriggerCount uint64 `json:"CurrentLevelTriggerCount"`
|
||||
LastLogs []string `json:"LastLogs"`
|
||||
}
|
||||
|
||||
func (g *BruteForceProtectionGroup) Reset() {
|
||||
g.LastTriggeredAtUnix = 0
|
||||
g.TriggerCount = 0
|
||||
g.CurrentLevelTriggerCount = 0
|
||||
g.LastLogs = []string{}
|
||||
}
|
||||
72
internal/daemon/db/repository/alert_group.go
Normal file
72
internal/daemon/db/repository/alert_group.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
bboltErrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
type AlertGroupRepository interface {
|
||||
Update(name string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type alertGroupRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewAlertGroupRepository(appDB *bbolt.DB) AlertGroupRepository {
|
||||
return &alertGroupRepository{
|
||||
db: appDB,
|
||||
bucket: alertGroupBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *alertGroupRepository) Update(name string, f func(*entity.AlertGroup) (*entity.AlertGroup, error)) error {
|
||||
entityAlertGroup := &entity.AlertGroup{}
|
||||
entityAlertGroup.Reset()
|
||||
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := []byte(name)
|
||||
|
||||
group := b.Get(key)
|
||||
if group != nil {
|
||||
err = json.Unmarshal(group, entityAlertGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal alert group: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entityAlertGroup, err = f(entityAlertGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entityAlertGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *alertGroupRepository) Clear() error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(r.bucket))
|
||||
if errors.Is(err, bboltErrors.ErrBucketNotFound) {
|
||||
// If the bucket may not exist, ignore ErrBucketNotFound
|
||||
return nil
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
173
internal/daemon/db/repository/blocking.go
Normal file
173
internal/daemon/db/repository/blocking.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
bboltErrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
type BlockingRepository interface {
|
||||
Add(blockedIP entity.Blocking) error
|
||||
List(callback func(entity.Blocking) error) error
|
||||
DeleteByIP(ip net.IP, callback func(entity.Blocking) error) error
|
||||
DeleteExpired(limit int) (int, error)
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type blocking struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewBlockingRepository(appDB *bbolt.DB) BlockingRepository {
|
||||
return &blocking{
|
||||
db: appDB,
|
||||
bucket: blockingBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *blocking) Add(blockedIP entity.Blocking) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(blockedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := nextKeyByExpire(bucket, uint64(blockedIP.ExpireAtUnix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(id, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *blocking) List(callback func(entity.Blocking) error) error {
|
||||
return r.db.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(r.bucket))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bucket.ForEach(func(_, v []byte) error {
|
||||
blockedIP := entity.Blocking{}
|
||||
err := json.Unmarshal(v, &blockedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := callback(blockedIP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *blocking) DeleteByIP(ip net.IP, callback func(entity.Blocking) error) error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := bucket.Cursor()
|
||||
|
||||
for k, v := c.First(); k != nil; {
|
||||
blockedIP := entity.Blocking{}
|
||||
err := json.Unmarshal(v, &blockedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedBlockedIP := net.ParseIP(blockedIP.IP)
|
||||
if parsedBlockedIP == nil || !parsedBlockedIP.Equal(ip) {
|
||||
k, v = c.Next()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := callback(blockedIP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextK, nextV := c.Next()
|
||||
if err := bucket.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
k = nextK
|
||||
v = nextV
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *blocking) DeleteExpired(limit int) (int, error) {
|
||||
if limit <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var deleted int
|
||||
err := r.db.Update(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
c := bucket.Cursor()
|
||||
deleted = 0
|
||||
|
||||
for k, v := c.First(); k != nil && deleted < limit; {
|
||||
blockedIP := entity.Blocking{}
|
||||
if err := json.Unmarshal(v, &blockedIP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if blockedIP.ExpireAtUnix <= 0 {
|
||||
k, v = c.Next()
|
||||
continue
|
||||
}
|
||||
|
||||
if blockedIP.ExpireAtUnix > now {
|
||||
// Not expired yet
|
||||
break
|
||||
}
|
||||
|
||||
nextK, nextV := c.Next()
|
||||
if err := bucket.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
deleted++
|
||||
k = nextK
|
||||
v = nextV
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
func (r *blocking) Clear() error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(r.bucket))
|
||||
if errors.Is(err, bboltErrors.ErrBucketNotFound) {
|
||||
// If the bucket may not exist, ignore ErrBucketNotFound
|
||||
return nil
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
103
internal/daemon/db/repository/brute_force_protection_group.go
Normal file
103
internal/daemon/db/repository/brute_force_protection_group.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
|
||||
"go.etcd.io/bbolt"
|
||||
bboltErrors "go.etcd.io/bbolt/errors"
|
||||
)
|
||||
|
||||
type BruteForceProtectionGroupRepository interface {
|
||||
Update(name string, ip net.IP, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error
|
||||
Clear() error
|
||||
}
|
||||
|
||||
type bruteForceProtectionGroupRepository struct {
|
||||
db *bbolt.DB
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewBruteForceProtectionGroupRepository(appDB *bbolt.DB) BruteForceProtectionGroupRepository {
|
||||
return &bruteForceProtectionGroupRepository{
|
||||
db: appDB,
|
||||
bucket: bruteForceProtectionGroupBucket,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *bruteForceProtectionGroupRepository) Update(name string, ip net.IP, f func(*entity.BruteForceProtectionGroup) (*entity.BruteForceProtectionGroup, error)) error {
|
||||
entityGroup := &entity.BruteForceProtectionGroup{}
|
||||
entityGroup.Reset()
|
||||
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := keyGroupIP(name, ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group := b.Get(key)
|
||||
if group != nil {
|
||||
err = json.Unmarshal(group, entityGroup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal brute force protection group: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entityGroup, err = f(entityGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entityGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put(key, data)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *bruteForceProtectionGroupRepository) Clear() error {
|
||||
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(r.bucket))
|
||||
if errors.Is(err, bboltErrors.ErrBucketNotFound) {
|
||||
// If the bucket may not exist, ignore ErrBucketNotFound
|
||||
return nil
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(r.bucket))
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func keyGroupIP(groupID string, ip net.IP) ([]byte, error) {
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("ip cannot be nil")
|
||||
}
|
||||
|
||||
if len(groupID) == 0 {
|
||||
return nil, fmt.Errorf("group id cannot be empty")
|
||||
}
|
||||
|
||||
if ip.To4() == nil && ip.To16() == nil {
|
||||
return nil, fmt.Errorf("ip is neither IPv4 nor IPv6")
|
||||
}
|
||||
|
||||
var ipAddr net.IP
|
||||
if ip.To4() != nil {
|
||||
ipAddr = ip.To4()
|
||||
} else {
|
||||
ipAddr = ip.To16()
|
||||
}
|
||||
|
||||
k := make([]byte, 0, len(groupID)+1+len(ipAddr))
|
||||
k = append(k, groupID...)
|
||||
k = append(k, 0x00)
|
||||
k = append(k, ipAddr...)
|
||||
return k, nil
|
||||
}
|
||||
@@ -27,7 +27,7 @@ type notificationsQueueRepository struct {
|
||||
func NewNotificationsQueueRepository(appDB *bbolt.DB) NotificationsQueueRepository {
|
||||
return ¬ificationsQueueRepository{
|
||||
db: appDB,
|
||||
bucket: notificationsQueue,
|
||||
bucket: notificationsQueueBucket,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ package repository
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationsQueue = "notifications_queue"
|
||||
notificationsQueueBucket = "notifications_queue"
|
||||
alertGroupBucket = "alert_group"
|
||||
bruteForceProtectionGroupBucket = "brute_force_protection_group"
|
||||
blockingBucket = "blocking"
|
||||
)
|
||||
|
||||
func nextID(b *bbolt.Bucket) ([]byte, error) {
|
||||
@@ -20,3 +24,24 @@ func nextID(b *bbolt.Bucket) ([]byte, error) {
|
||||
binary.BigEndian.PutUint64(key, seq)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func nextKeyByExpire(b *bbolt.Bucket, expireUnixAt uint64) ([]byte, error) {
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 0 = "forever" -> sort after any finite timestamp
|
||||
if expireUnixAt == 0 {
|
||||
expireUnixAt = math.MaxUint64
|
||||
}
|
||||
|
||||
// 8 bytes expire + 8 bytes seq
|
||||
key := make([]byte, 16)
|
||||
|
||||
// Important: BigEndian, so that sorting by bytes matches sorting by number.
|
||||
binary.BigEndian.PutUint64(key[0:8], expireUnixAt)
|
||||
binary.BigEndian.PutUint64(key[8:16], seq)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
257
internal/daemon/firewall/blocking/blocking.go
Normal file
257
internal/daemon/firewall/blocking/blocking.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package blocking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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/daemon/firewall/chain/block"
|
||||
"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/log"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
NftReload(blockListIP block.ListIP, blockListIPWithPort block.ListIPWithPort) error
|
||||
BlockIP(block BlockIP) (bool, error)
|
||||
BlockIPWithPorts(block BlockIPWithPorts) (bool, error)
|
||||
UnblockAllIPs() error
|
||||
UnblockIP(ip net.IP) error
|
||||
ClearDBData() error
|
||||
}
|
||||
|
||||
type blocking struct {
|
||||
blockingRepository repository.BlockingRepository
|
||||
blockListIP block.ListIP
|
||||
blockListIPWithPort block.ListIPWithPort
|
||||
logger log.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type BlockIP struct {
|
||||
IP net.IP
|
||||
TimeSeconds uint32
|
||||
Reason string
|
||||
}
|
||||
|
||||
type BlockIPWithPorts struct {
|
||||
IP net.IP
|
||||
TimeSeconds uint32
|
||||
Reason string
|
||||
Ports []types.L4Port
|
||||
}
|
||||
|
||||
func New(blockingRepository repository.BlockingRepository, logger log.Logger) API {
|
||||
return &blocking{
|
||||
blockingRepository: blockingRepository,
|
||||
logger: logger,
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *blocking) NftReload(blockListIP block.ListIP, blockListIPWithPort block.ListIPWithPort) error {
|
||||
b.mu.Lock()
|
||||
b.blockListIP = blockListIP
|
||||
b.blockListIPWithPort = blockListIPWithPort
|
||||
b.mu.Unlock()
|
||||
|
||||
isExpiredEntries := false
|
||||
nowUnix := time.Now().Unix()
|
||||
err := b.blockingRepository.List(func(e entity.Blocking) error {
|
||||
ip := net.ParseIP(e.IP)
|
||||
if ip == nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to parse IP address: %s", e.IP))
|
||||
return nil
|
||||
}
|
||||
|
||||
blockSeconds := uint32(0)
|
||||
if e.ExpireAtUnix > 0 {
|
||||
if e.ExpireAtUnix < nowUnix {
|
||||
isExpiredEntries = true
|
||||
return nil
|
||||
}
|
||||
blockSeconds = uint32(e.ExpireAtUnix - nowUnix)
|
||||
}
|
||||
|
||||
if e.IsPorts() {
|
||||
l4Ports, err := e.ToL4Ports()
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to parse ports: %s", err))
|
||||
return nil
|
||||
}
|
||||
if err := b.blockListIPWithPort.AddIP(ip, l4Ports, blockSeconds); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to add IP %s to block list: %s", ip.String(), err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := b.blockListIP.AddIP(ip, blockSeconds); err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to add IP %s to block list: %s", ip.String(), err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if isExpiredEntries {
|
||||
go func() {
|
||||
deleteCount, err := b.blockingRepository.DeleteExpired(100)
|
||||
if err != nil {
|
||||
b.logger.Error(fmt.Sprintf("Failed to delete expired entries from database: %s", err))
|
||||
}
|
||||
b.logger.Debug(fmt.Sprintf("Deleted %d expired entries from database", deleteCount))
|
||||
}()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *blocking) BlockIP(block BlockIP) (bool, error) {
|
||||
if block.IP.IsLoopback() {
|
||||
return false, fmt.Errorf("loopback IP address %s cannot be blocked", block.IP.String())
|
||||
}
|
||||
|
||||
if err := b.blockListIP.AddIP(block.IP, block.TimeSeconds); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
expireAtUnix := int64(0)
|
||||
if block.TimeSeconds > 0 {
|
||||
expire := time.Now().Add(time.Duration(int64(block.TimeSeconds)) * time.Second)
|
||||
expireAtUnix = expire.Unix()
|
||||
}
|
||||
data := entity.Blocking{
|
||||
IP: block.IP.String(),
|
||||
ExpireAtUnix: expireAtUnix,
|
||||
Reason: block.Reason,
|
||||
}
|
||||
if err := b.blockingRepository.Add(data); err != nil {
|
||||
return true, fmt.Errorf("the IP is blocked, but not recorded in the database. Failed to add IP %s to database: %w", block.IP.String(), err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *blocking) BlockIPWithPorts(block BlockIPWithPorts) (bool, error) {
|
||||
if block.IP.IsLoopback() {
|
||||
return false, fmt.Errorf("loopback IP address %s cannot be blocked", block.IP.String())
|
||||
}
|
||||
|
||||
if err := b.blockListIPWithPort.AddIP(block.IP, block.Ports, block.TimeSeconds); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var l4Ports []entity.BlockingPort
|
||||
for _, port := range block.Ports {
|
||||
l4Ports = append(l4Ports, entity.BlockingPort{
|
||||
Number: port.Number(),
|
||||
Protocol: port.ProtocolString(),
|
||||
})
|
||||
}
|
||||
|
||||
expireAtUnix := int64(0)
|
||||
if block.TimeSeconds > 0 {
|
||||
expire := time.Now().Add(time.Duration(int64(block.TimeSeconds)) * time.Second)
|
||||
expireAtUnix = expire.Unix()
|
||||
}
|
||||
data := entity.Blocking{
|
||||
IP: block.IP.String(),
|
||||
ExpireAtUnix: expireAtUnix,
|
||||
Reason: block.Reason,
|
||||
Ports: l4Ports,
|
||||
}
|
||||
if err := b.blockingRepository.Add(data); err != nil {
|
||||
return true, fmt.Errorf("the IP is blocked, but not recorded in the database. Failed to add IP %s to database: %w", block.IP.String(), err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *blocking) UnblockIP(ip net.IP) error {
|
||||
err := b.blockingRepository.DeleteByIP(ip, func(e entity.Blocking) error {
|
||||
if e.IsPorts() {
|
||||
l4Ports, err := e.ToL4Ports()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.removeIPWithPorts(ip, l4Ports)
|
||||
}
|
||||
|
||||
if err := b.blockListIP.DeleteIP(ip); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *blocking) UnblockAllIPs() error {
|
||||
err := b.blockingRepository.List(func(e entity.Blocking) error {
|
||||
ip := net.ParseIP(e.IP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("failed to parse IP address: %s", e.IP)
|
||||
}
|
||||
|
||||
if e.IsPorts() {
|
||||
l4Ports, err := e.ToL4Ports()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, port := range l4Ports {
|
||||
if err := b.blockListIPWithPort.DeleteIP(ip, port); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") ||
|
||||
strings.Contains(err.Error(), "Error: Could not process rule: No such file or directory") {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.blockListIP.DeleteIP(ip); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
_ = b.blockingRepository.Clear()
|
||||
return err
|
||||
}
|
||||
|
||||
return b.blockingRepository.Clear()
|
||||
}
|
||||
|
||||
func (b *blocking) ClearDBData() error {
|
||||
return b.blockingRepository.Clear()
|
||||
}
|
||||
|
||||
func (b *blocking) removeIPWithPorts(ip net.IP, l4Ports []types.L4Port) error {
|
||||
for _, port := range l4Ports {
|
||||
if err := b.blockListIPWithPort.DeleteIP(ip, port); err != nil {
|
||||
if strings.Contains(err.Error(), "element does not exist") ||
|
||||
strings.Contains(err.Error(), "Error: Could not process rule: No such file or directory") {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
41
internal/daemon/firewall/chain/after_local_input.go
Normal file
41
internal/daemon/firewall/chain/after_local_input.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type AfterLocalInput interface {
|
||||
AddRule(expr ...string) error
|
||||
AddRuleIn(AddRuleFunc func(expr ...string) error) error
|
||||
}
|
||||
|
||||
type afterLocalInput struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
func newAfterLocalInput(nft nft.NFT, family family.Type, table string) (LocalInput, error) {
|
||||
chain := "after-local-input"
|
||||
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &afterLocalInput{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *afterLocalInput) AddRule(expr ...string) error {
|
||||
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
|
||||
}
|
||||
|
||||
func (l *afterLocalInput) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
|
||||
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
|
||||
}
|
||||
41
internal/daemon/firewall/chain/before_local_input.go
Normal file
41
internal/daemon/firewall/chain/before_local_input.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package chain
|
||||
|
||||
import (
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type BeforeLocalInput interface {
|
||||
AddRule(expr ...string) error
|
||||
AddRuleIn(AddRuleFunc func(expr ...string) error) error
|
||||
}
|
||||
|
||||
type beforeLocalInput struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
chain string
|
||||
}
|
||||
|
||||
func newBeforeLocalInput(nft nft.NFT, family family.Type, table string) (LocalInput, error) {
|
||||
chain := "before-local-input"
|
||||
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &beforeLocalInput{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
chain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *beforeLocalInput) AddRule(expr ...string) error {
|
||||
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
|
||||
}
|
||||
|
||||
func (l *beforeLocalInput) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
|
||||
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
|
||||
}
|
||||
59
internal/daemon/firewall/chain/block/list.go
Normal file
59
internal/daemon/firewall/chain/block/list.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type List interface {
|
||||
Name() string
|
||||
AddElement(element string) error
|
||||
DeleteElement(element string) error
|
||||
}
|
||||
|
||||
type list struct {
|
||||
nft nft.NFT
|
||||
family family.Type
|
||||
table string
|
||||
name string
|
||||
}
|
||||
|
||||
func newList(nft nft.NFT, family family.Type, table string, name string, params string) (List, error) {
|
||||
command := []string{
|
||||
"add set", family.String(), table, name, "{ " + params + " }",
|
||||
}
|
||||
if err := nft.Command().Run(command...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &list{
|
||||
nft: nft,
|
||||
family: family,
|
||||
table: table,
|
||||
name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *list) Name() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l *list) AddElement(element string) error {
|
||||
command := []string{
|
||||
"add element",
|
||||
l.family.String(), l.table, l.name,
|
||||
fmt.Sprintf("{ %s }", element),
|
||||
}
|
||||
return l.nft.Command().Run(command...)
|
||||
}
|
||||
|
||||
func (l *list) DeleteElement(element string) error {
|
||||
command := []string{
|
||||
"delete element",
|
||||
l.family.String(), l.table, l.name,
|
||||
fmt.Sprintf("{ %s }", element),
|
||||
}
|
||||
return l.nft.Command().Run(command...)
|
||||
}
|
||||
87
internal/daemon/firewall/chain/block/list_ip.go
Normal file
87
internal/daemon/firewall/chain/block/list_ip.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
)
|
||||
|
||||
type ListIP interface {
|
||||
// AddIP Add an IP address to the list.
|
||||
AddIP(addr net.IP, banSeconds uint32) error
|
||||
|
||||
// DeleteIP Delete an IP address from the list.
|
||||
DeleteIP(addr net.IP) error
|
||||
|
||||
// AddRuleToChain Add a rule to the parent chain.
|
||||
AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error
|
||||
}
|
||||
|
||||
type listIP struct {
|
||||
listIPv4 List
|
||||
listIPv6 List
|
||||
}
|
||||
|
||||
func NewListIP(nft nft.NFT, family family.Type, table string, name string) (ListIP, error) {
|
||||
params := "type ipv4_addr; flags interval, timeout;"
|
||||
listName := name + "_ip4"
|
||||
listIPv4, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params = "type ipv6_addr; flags interval, timeout;"
|
||||
listName = name + "_ip6"
|
||||
listIPv6, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &listIP{
|
||||
listIPv4: listIPv4,
|
||||
listIPv6: listIPv6,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *listIP) AddIP(addr net.IP, banSeconds uint32) error {
|
||||
el := []string{addr.String()}
|
||||
if banSeconds > 0 {
|
||||
el = append(el, "timeout", fmt.Sprintf("%ds", banSeconds))
|
||||
}
|
||||
|
||||
element := strings.Join(el, " ")
|
||||
|
||||
if addr.To4() != nil {
|
||||
return l.listIPv4.AddElement(element)
|
||||
}
|
||||
|
||||
return l.listIPv6.AddElement(element)
|
||||
}
|
||||
|
||||
func (l *listIP) DeleteIP(addr net.IP) error {
|
||||
if addr == nil {
|
||||
return fmt.Errorf("IP address cannot be nil")
|
||||
}
|
||||
if addr.To4() != nil {
|
||||
return l.listIPv4.DeleteElement(addr.String())
|
||||
}
|
||||
|
||||
return l.listIPv6.DeleteElement(addr.String())
|
||||
}
|
||||
|
||||
func (l *listIP) AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error {
|
||||
rule := "ip saddr @" + l.listIPv4.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rule = "ip6 saddr @" + l.listIPv6.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
102
internal/daemon/firewall/chain/block/list_ip_port.go
Normal file
102
internal/daemon/firewall/chain/block/list_ip_port.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
)
|
||||
|
||||
type ListIPWithPort interface {
|
||||
// AddIP Add an IP address to the list.
|
||||
AddIP(addr net.IP, ports []types.L4Port, banSeconds uint32) error
|
||||
|
||||
// DeleteIP Delete an IP address from the list.
|
||||
DeleteIP(addr net.IP, port types.L4Port) error
|
||||
|
||||
// AddRuleToChain Add a rule to the parent chain.
|
||||
AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error
|
||||
}
|
||||
|
||||
type listIPWithPort struct {
|
||||
listIPv4 List
|
||||
listIPv6 List
|
||||
}
|
||||
|
||||
func NewListIPWithPort(nft nft.NFT, family family.Type, table string, name string) (ListIPWithPort, error) {
|
||||
params := "type ipv4_addr . inet_proto . inet_service; flags interval, timeout;"
|
||||
listName := name + "_ip4"
|
||||
listIPv4, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params = "type ipv6_addr . inet_proto . inet_service; flags interval, timeout;"
|
||||
listName = name + "_ip6"
|
||||
listIPv6, err := newList(nft, family, table, listName, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &listIPWithPort{
|
||||
listIPv4: listIPv4,
|
||||
listIPv6: listIPv6,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *listIPWithPort) AddIP(addr net.IP, ports []types.L4Port, banSeconds uint32) error {
|
||||
if len(ports) == 0 {
|
||||
return fmt.Errorf("ports is empty")
|
||||
}
|
||||
|
||||
var elements []string
|
||||
for _, port := range ports {
|
||||
el := []string{fmt.Sprintf("%s . %s . %d", addr.String(), port.ProtocolString(), port.Number())}
|
||||
if banSeconds > 0 {
|
||||
el = append(el, "timeout", fmt.Sprintf("%ds", banSeconds))
|
||||
}
|
||||
|
||||
elements = append(elements, strings.Join(el, " "))
|
||||
}
|
||||
|
||||
element := strings.Join(elements, ",")
|
||||
if addr.To4() != nil {
|
||||
return l.listIPv4.AddElement(element)
|
||||
}
|
||||
|
||||
return l.listIPv6.AddElement(element)
|
||||
}
|
||||
|
||||
func (l *listIPWithPort) DeleteIP(addr net.IP, port types.L4Port) error {
|
||||
if addr == nil {
|
||||
return fmt.Errorf("IP address cannot be nil")
|
||||
}
|
||||
if port.ToString() == "" {
|
||||
return fmt.Errorf("port cannot be empty")
|
||||
}
|
||||
|
||||
element := fmt.Sprintf("%s . %s . %d", addr.String(), port.ProtocolString(), port.Number())
|
||||
|
||||
if addr.To4() != nil {
|
||||
return l.listIPv4.DeleteElement(element)
|
||||
}
|
||||
|
||||
return l.listIPv6.DeleteElement(element)
|
||||
}
|
||||
|
||||
func (l *listIPWithPort) AddRuleToChain(chainAddRuleFunc func(expr ...string) error, action string) error {
|
||||
rule := "ip saddr . meta l4proto . th dport @" + l.listIPv4.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rule = "ip6 saddr . meta l4proto . th dport @" + l.listIPv6.Name() + " " + action
|
||||
if err := chainAddRuleFunc(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
|
||||
nftFamily "git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain/block"
|
||||
)
|
||||
|
||||
type Chains interface {
|
||||
@@ -21,9 +22,15 @@ type Chains interface {
|
||||
NewForward(chain string, defaultAllow bool, priority int) error
|
||||
Forward() Forward
|
||||
|
||||
NewBeforeLocalInput() error
|
||||
BeforeLocalInput() BeforeLocalInput
|
||||
|
||||
NewLocalInput() error
|
||||
LocalInput() LocalInput
|
||||
|
||||
NewAfterLocalInput() error
|
||||
AfterLocalInput() AfterLocalInput
|
||||
|
||||
NewLocalOutput() error
|
||||
LocalOutput() LocalOutput
|
||||
|
||||
@@ -34,6 +41,8 @@ type Chains interface {
|
||||
|
||||
NewNoneChain(chain string) (Chain, error)
|
||||
NewChain(chain string, baseChain nftChain.ChainOptions) (Chain, error)
|
||||
NewBlockListIP(name string) (block.ListIP, error)
|
||||
NewBlockListIPWithPort(name string) (block.ListIPWithPort, error)
|
||||
}
|
||||
|
||||
type chains struct {
|
||||
@@ -42,7 +51,10 @@ type chains struct {
|
||||
forward Forward
|
||||
packetFilter PacketFilter
|
||||
|
||||
localInput LocalInput
|
||||
beforeLocalInput BeforeLocalInput
|
||||
localInput LocalInput
|
||||
afterLocalInput AfterLocalInput
|
||||
|
||||
localOutput LocalOutput
|
||||
localForward LocalForward
|
||||
|
||||
@@ -125,6 +137,19 @@ func (c *chains) Forward() Forward {
|
||||
return c.forward
|
||||
}
|
||||
|
||||
func (c *chains) NewBeforeLocalInput() error {
|
||||
newChain, err := newBeforeLocalInput(c.nft, c.family, c.table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.beforeLocalInput = newChain
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) BeforeLocalInput() BeforeLocalInput {
|
||||
return c.beforeLocalInput
|
||||
}
|
||||
|
||||
func (c *chains) NewLocalInput() error {
|
||||
localInput, err := newLocalInput(c.nft, c.family, c.table)
|
||||
if err != nil {
|
||||
@@ -138,6 +163,19 @@ func (c *chains) LocalInput() LocalInput {
|
||||
return c.localInput
|
||||
}
|
||||
|
||||
func (c *chains) NewAfterLocalInput() error {
|
||||
newChain, err := newAfterLocalInput(c.nft, c.family, c.table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.afterLocalInput = newChain
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chains) AfterLocalInput() AfterLocalInput {
|
||||
return c.afterLocalInput
|
||||
}
|
||||
|
||||
func (c *chains) NewLocalOutput() error {
|
||||
localOutput, err := newLocalOutput(c.nft, c.family, c.table)
|
||||
if err != nil {
|
||||
@@ -185,6 +223,24 @@ func (c *chains) NewChain(chainName string, baseChain nftChain.ChainOptions) (Ch
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *chains) NewBlockListIP(name string) (block.ListIP, error) {
|
||||
blockList, err := block.NewListIP(c.nft, c.family, c.table, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blockList, nil
|
||||
}
|
||||
|
||||
func (c *chains) NewBlockListIPWithPort(name string) (block.ListIPWithPort, error) {
|
||||
blockList, err := block.NewListIPWithPort(c.nft, c.family, c.table, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blockList, nil
|
||||
}
|
||||
|
||||
func clearRules(nft nft.NFT, family nftFamily.Type, table string) error {
|
||||
if err := nft.Table().Delete(family, table); err != nil {
|
||||
if !strings.Contains(string(err.Error()), "delete table "+family.String()+" "+table) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -35,32 +35,14 @@ type ConfigPolicy struct {
|
||||
DefaultAllowInput bool
|
||||
DefaultAllowOutput bool
|
||||
DefaultAllowForward bool
|
||||
InputDrop PolicyDrop
|
||||
InputDrop types.PolicyDrop
|
||||
InputPriority int
|
||||
OutputDrop PolicyDrop
|
||||
OutputDrop types.PolicyDrop
|
||||
OutputPriority int
|
||||
ForwardDrop PolicyDrop
|
||||
ForwardDrop types.PolicyDrop
|
||||
ForwardPriority int
|
||||
}
|
||||
|
||||
type PolicyDrop int8
|
||||
|
||||
const (
|
||||
Drop PolicyDrop = iota + 1
|
||||
Reject
|
||||
)
|
||||
|
||||
func (p PolicyDrop) String() string {
|
||||
switch p {
|
||||
case Drop:
|
||||
return "drop"
|
||||
case Reject:
|
||||
return "reject"
|
||||
default:
|
||||
return "drop"
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigIP4 struct {
|
||||
IcmpIn bool
|
||||
IcmpInRate string
|
||||
@@ -79,78 +61,19 @@ type ConfigIP6 struct {
|
||||
}
|
||||
|
||||
type ConfigPort struct {
|
||||
Number uint16
|
||||
Protocol Protocol
|
||||
Action Action
|
||||
Port types.L4Port
|
||||
Action types.Action
|
||||
LimitRate string
|
||||
}
|
||||
|
||||
type ConfigIP struct {
|
||||
IP string
|
||||
OnlyIP bool // Port is not taken into account
|
||||
Port uint16
|
||||
Action Action
|
||||
Protocol Protocol
|
||||
Port types.L4Port
|
||||
Action types.Action
|
||||
LimitRate string
|
||||
}
|
||||
|
||||
type Action int8
|
||||
|
||||
const (
|
||||
ActionAccept Action = iota + 1
|
||||
ActionReject
|
||||
ActionDrop
|
||||
)
|
||||
|
||||
func (a Action) String() string {
|
||||
switch a {
|
||||
case ActionAccept:
|
||||
return "accept"
|
||||
case ActionReject:
|
||||
return "reject"
|
||||
case ActionDrop:
|
||||
return "drop"
|
||||
default:
|
||||
return "drop"
|
||||
}
|
||||
}
|
||||
|
||||
type Protocol int8
|
||||
|
||||
const (
|
||||
ProtocolTCP Protocol = iota + 1
|
||||
ProtocolUDP
|
||||
)
|
||||
|
||||
func (p Protocol) String() string {
|
||||
switch p {
|
||||
case ProtocolTCP:
|
||||
return "tcp"
|
||||
case ProtocolUDP:
|
||||
return "udp"
|
||||
default:
|
||||
return fmt.Sprintf("Protocol(%d)", p)
|
||||
}
|
||||
}
|
||||
|
||||
type Direction int8
|
||||
|
||||
const (
|
||||
DirectionIn Direction = iota + 1
|
||||
DirectionOut
|
||||
)
|
||||
|
||||
func (d Direction) String() string {
|
||||
switch d {
|
||||
case DirectionIn:
|
||||
return "in"
|
||||
case DirectionOut:
|
||||
return "out"
|
||||
default:
|
||||
return fmt.Sprintf("Direction(%d)", d)
|
||||
}
|
||||
}
|
||||
|
||||
type ClearMode int8
|
||||
|
||||
const (
|
||||
|
||||
@@ -2,9 +2,11 @@ package firewall
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"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/blocking"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/chain"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
|
||||
@@ -21,28 +23,46 @@ type API interface {
|
||||
// ClearRules Clear all rules.
|
||||
ClearRules()
|
||||
|
||||
// BlockIP Block IP address.
|
||||
BlockIP(blockIP blocking.BlockIP) (bool, error)
|
||||
|
||||
// BlockIPWithPorts Block IP address with ports.
|
||||
BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error)
|
||||
|
||||
// UnblockAllIPs Unblock all IP addresses.
|
||||
UnblockAllIPs() error
|
||||
|
||||
// UnblockIP Unblock IP address.
|
||||
UnblockIP(ip net.IP) error
|
||||
|
||||
// ClearDBData Clear all data from DB
|
||||
ClearDBData() error
|
||||
|
||||
// DockerSupport Return true if docker support
|
||||
DockerSupport() bool
|
||||
}
|
||||
|
||||
type firewall struct {
|
||||
nft nftables.NFT
|
||||
logger log.Logger
|
||||
config *Config
|
||||
chains chain.Chains
|
||||
docker docker_monitor.Docker
|
||||
nft nftables.NFT
|
||||
logger log.Logger
|
||||
config *Config
|
||||
blockingService blocking.API
|
||||
chains chain.Chains
|
||||
docker docker_monitor.Docker
|
||||
}
|
||||
|
||||
func New(pathNFT string, logger log.Logger, config Config, docker docker_monitor.Docker) (API, error) {
|
||||
func New(pathNFT string, blockingService blocking.API, logger log.Logger, config Config, docker docker_monitor.Docker) (API, error) {
|
||||
nft, err := nftables.NewWithPath(pathNFT)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create nft client: %w %s", err, pathNFT)
|
||||
}
|
||||
|
||||
return &firewall{
|
||||
nft: nft,
|
||||
logger: logger,
|
||||
config: &config,
|
||||
docker: docker,
|
||||
nft: nft,
|
||||
logger: logger,
|
||||
config: &config,
|
||||
blockingService: blockingService,
|
||||
docker: docker,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -82,6 +102,10 @@ func (f *firewall) Reload() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.reloadBlockList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.logger.Debug("Reload nftables rules done")
|
||||
return nil
|
||||
}
|
||||
@@ -105,6 +129,18 @@ func (f *firewall) ClearRules() {
|
||||
f.logger.Debug("Clear nftables rules done")
|
||||
}
|
||||
|
||||
func (f *firewall) UnblockAllIPs() error {
|
||||
return f.blockingService.UnblockAllIPs()
|
||||
}
|
||||
|
||||
func (f *firewall) UnblockIP(ip net.IP) error {
|
||||
return f.blockingService.UnblockIP(ip)
|
||||
}
|
||||
|
||||
func (f *firewall) ClearDBData() error {
|
||||
return f.blockingService.ClearDBData()
|
||||
}
|
||||
|
||||
func (f *firewall) SavesRules() {
|
||||
if !f.config.Options.SavesRules {
|
||||
f.logger.Debug("SavesRules is false, skip")
|
||||
@@ -133,6 +169,24 @@ func (f *firewall) SavesRules() {
|
||||
f.logger.Info("Save nftables rules")
|
||||
}
|
||||
|
||||
func (f *firewall) BlockIP(blockIP blocking.BlockIP) (bool, error) {
|
||||
isBanned, err := f.blockingService.BlockIP(blockIP)
|
||||
|
||||
if err != nil {
|
||||
f.logger.Warn(fmt.Sprintf("Failed to block ip %s: %s", blockIP.IP.String(), err))
|
||||
}
|
||||
return isBanned, err
|
||||
}
|
||||
|
||||
func (f *firewall) BlockIPWithPorts(blockIP blocking.BlockIPWithPorts) (bool, error) {
|
||||
isBanned, err := f.blockingService.BlockIPWithPorts(blockIP)
|
||||
|
||||
if err != nil {
|
||||
f.logger.Warn(fmt.Sprintf("Failed to block ip %s: %s", blockIP.IP.String(), err))
|
||||
}
|
||||
return isBanned, err
|
||||
}
|
||||
|
||||
func (f *firewall) DockerSupport() bool {
|
||||
return f.config.Options.DockerSupport
|
||||
}
|
||||
|
||||
25
internal/daemon/firewall/reload_block_list.go
Normal file
25
internal/daemon/firewall/reload_block_list.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package firewall
|
||||
|
||||
func (f *firewall) reloadBlockList() error {
|
||||
listBlockedIP, err := f.chains.NewBlockListIP("blocked_ip")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := listBlockedIP.AddRuleToChain(f.chains.BeforeLocalInput().AddRule, "drop"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listBlockedIPWithPort, err := f.chains.NewBlockListIPWithPort("blocked_ip_port")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := listBlockedIPWithPort.AddRuleToChain(f.chains.BeforeLocalInput().AddRule, "drop"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.blockingService.NftReload(listBlockedIP, listBlockedIPWithPort); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package firewall
|
||||
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
|
||||
|
||||
func (f *firewall) reloadForward() error {
|
||||
f.logger.Debug("Reloading forward chain")
|
||||
err := f.chains.NewForward(f.config.MetadataNaming.ChainForwardName, f.config.Policy.DefaultAllowForward, f.config.Policy.ForwardPriority)
|
||||
@@ -38,7 +40,7 @@ func (f *firewall) reloadForwardAddIPs() error {
|
||||
}
|
||||
|
||||
for _, ipConfig := range f.config.IP4.InIPs {
|
||||
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||
if ipConfig.Action != types.ActionDrop && ipConfig.Action != types.ActionReject {
|
||||
continue
|
||||
}
|
||||
if err := forwardAddIP(chain.AddRule, ipConfig, "ip"); err != nil {
|
||||
@@ -51,7 +53,7 @@ func (f *firewall) reloadForwardAddIPs() error {
|
||||
}
|
||||
|
||||
for _, ipConfig := range f.config.IP6.InIPs {
|
||||
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||
if ipConfig.Action != types.ActionDrop && ipConfig.Action != types.ActionReject {
|
||||
continue
|
||||
}
|
||||
if err := forwardAddIP(chain.AddRule, ipConfig, "ip6"); err != nil {
|
||||
|
||||
@@ -3,7 +3,6 @@ package firewall
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg"
|
||||
)
|
||||
@@ -24,10 +23,24 @@ func (f *firewall) reloadInput() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.chains.NewBeforeLocalInput(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.chains.BeforeLocalInput().AddRuleIn(chain.AddRule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.reloadInputAddIPs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.chains.NewAfterLocalInput(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.chains.AfterLocalInput().AddRuleIn(chain.AddRule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.chains.PacketFilter().AddRuleIn(chain.AddRule); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -201,8 +214,8 @@ func (f *firewall) reloadInputICMP6Strict() error {
|
||||
func (f *firewall) reloadInputPorts() error {
|
||||
chain := f.chains.Input()
|
||||
for _, port := range f.config.InPorts {
|
||||
protocol := port.Protocol.String()
|
||||
number := strconv.Itoa(int(port.Number))
|
||||
protocol := port.Port.ProtocolString()
|
||||
number := port.Port.NumberString()
|
||||
|
||||
baseRule := "iifname != \"lo\" meta l4proto " + protocol + " ct state new " + protocol + " dport " + number
|
||||
|
||||
@@ -256,7 +269,7 @@ func inputAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatch
|
||||
|
||||
rule := ipMatch + " saddr " + config.IP + " iifname != \"lo\""
|
||||
if !config.OnlyIP {
|
||||
rule += " " + config.Protocol.String() + " dport " + strconv.Itoa(int(config.Port))
|
||||
rule += " " + config.Port.ProtocolString() + " dport " + config.Port.NumberString()
|
||||
}
|
||||
if config.LimitRate != "" {
|
||||
rule += " limit rate " + config.LimitRate
|
||||
|
||||
@@ -3,7 +3,6 @@ package firewall
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg"
|
||||
)
|
||||
@@ -176,8 +175,8 @@ func (f *firewall) reloadOutputICMPAfter() error {
|
||||
func (f *firewall) reloadOutputPorts() error {
|
||||
chain := f.chains.Output()
|
||||
for _, port := range f.config.OutPorts {
|
||||
protocol := port.Protocol.String()
|
||||
number := strconv.Itoa(int(port.Number))
|
||||
protocol := port.Port.ProtocolString()
|
||||
number := port.Port.NumberString()
|
||||
baseRule := "oifname != \"lo\" meta l4proto " + protocol + " ct state new " + protocol + " dport " + number
|
||||
|
||||
if port.LimitRate != "" {
|
||||
@@ -231,7 +230,7 @@ func outputAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatc
|
||||
|
||||
rule := ipMatch + " daddr " + config.IP + " oifname != \"lo\""
|
||||
if !config.OnlyIP {
|
||||
rule += " " + config.Protocol.String() + " dport " + strconv.Itoa(int(config.Port))
|
||||
rule += " " + config.Port.ProtocolString() + " dport " + config.Port.NumberString()
|
||||
}
|
||||
if config.LimitRate != "" {
|
||||
rule += " limit rate " + config.LimitRate
|
||||
|
||||
43
internal/daemon/firewall/types/port.go
Normal file
43
internal/daemon/firewall/types/port.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type L4Port interface {
|
||||
Number() uint16
|
||||
NumberString() string
|
||||
ProtocolString() string
|
||||
ToString() string
|
||||
}
|
||||
|
||||
type l4Port struct {
|
||||
number uint16
|
||||
protocol string
|
||||
}
|
||||
|
||||
func NewL4Port(number uint16, protocol Protocol) (L4Port, error) {
|
||||
if protocol != ProtocolTCP && protocol != ProtocolUDP {
|
||||
return nil, errors.New("invalid protocol")
|
||||
}
|
||||
|
||||
return &l4Port{number: number, protocol: protocol.String()}, nil
|
||||
}
|
||||
|
||||
func (p *l4Port) Number() uint16 {
|
||||
return p.number
|
||||
}
|
||||
|
||||
func (p *l4Port) NumberString() string {
|
||||
port := p.Number()
|
||||
return strconv.Itoa(int(port))
|
||||
}
|
||||
|
||||
func (p *l4Port) ProtocolString() string {
|
||||
return p.protocol
|
||||
}
|
||||
|
||||
func (p *l4Port) ToString() string {
|
||||
return p.NumberString() + "/" + p.ProtocolString()
|
||||
}
|
||||
78
internal/daemon/firewall/types/types.go
Normal file
78
internal/daemon/firewall/types/types.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
type PolicyDrop int8
|
||||
|
||||
const (
|
||||
Drop PolicyDrop = iota + 1
|
||||
Reject
|
||||
)
|
||||
|
||||
func (p PolicyDrop) String() string {
|
||||
switch p {
|
||||
case Drop:
|
||||
return "drop"
|
||||
case Reject:
|
||||
return "reject"
|
||||
default:
|
||||
return "drop"
|
||||
}
|
||||
}
|
||||
|
||||
type Action int8
|
||||
|
||||
const (
|
||||
ActionAccept Action = iota + 1
|
||||
ActionReject
|
||||
ActionDrop
|
||||
)
|
||||
|
||||
func (a Action) String() string {
|
||||
switch a {
|
||||
case ActionAccept:
|
||||
return "accept"
|
||||
case ActionReject:
|
||||
return "reject"
|
||||
case ActionDrop:
|
||||
return "drop"
|
||||
default:
|
||||
return "drop"
|
||||
}
|
||||
}
|
||||
|
||||
type Protocol int8
|
||||
|
||||
const (
|
||||
ProtocolTCP Protocol = iota + 1
|
||||
ProtocolUDP
|
||||
)
|
||||
|
||||
func (p Protocol) String() string {
|
||||
switch p {
|
||||
case ProtocolTCP:
|
||||
return "tcp"
|
||||
case ProtocolUDP:
|
||||
return "udp"
|
||||
default:
|
||||
return fmt.Sprintf("Protocol(%d)", p)
|
||||
}
|
||||
}
|
||||
|
||||
type Direction int8
|
||||
|
||||
const (
|
||||
DirectionIn Direction = iota + 1
|
||||
DirectionOut
|
||||
)
|
||||
|
||||
func (d Direction) String() string {
|
||||
switch d {
|
||||
case DirectionIn:
|
||||
return "in"
|
||||
case DirectionOut:
|
||||
return "out"
|
||||
default:
|
||||
return fmt.Sprintf("Direction(%d)", d)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package daemon
|
||||
|
||||
import (
|
||||
"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"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
|
||||
)
|
||||
|
||||
@@ -12,4 +13,5 @@ type DaemonOptions struct {
|
||||
PathNftables string
|
||||
ConfigFirewall firewall.Config
|
||||
ConfigAnalyzer config.Config
|
||||
Repositories db.Repositories
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer"
|
||||
"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/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/notifications"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/pidfile"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
|
||||
@@ -27,9 +29,11 @@ func NewDaemon(opts DaemonOptions, logger log.Logger, notifications notification
|
||||
return nil, err
|
||||
}
|
||||
|
||||
firewall, err := firewall2.New(opts.PathNftables, logger, opts.ConfigFirewall, docker)
|
||||
blockingService := blocking.New(opts.Repositories.Blocking(), logger)
|
||||
firewall, err := firewall2.New(opts.PathNftables, blockingService, logger, opts.ConfigFirewall, docker)
|
||||
|
||||
analyzerService := analyzer.New(opts.ConfigAnalyzer, logger, notifications)
|
||||
blockService := brute_force_protection_group.NewBlockService(firewall.BlockIP, firewall.BlockIPWithPorts)
|
||||
analyzerService := analyzer.New(opts.ConfigAnalyzer, blockService, opts.Repositories, logger, notifications)
|
||||
|
||||
return &daemon{
|
||||
pidFile: pidFile,
|
||||
|
||||
@@ -2,6 +2,7 @@ package socket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -11,7 +12,12 @@ import (
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||
)
|
||||
|
||||
type HandleCommand func(command string, socket Connect) error
|
||||
type Message struct {
|
||||
Command string `json:"command"`
|
||||
Args map[string]string `json:"args"`
|
||||
}
|
||||
|
||||
type HandleCommand func(command string, args map[string]string, socket Connect) error
|
||||
|
||||
type Socket interface {
|
||||
EnsureNoOtherProcess() error
|
||||
@@ -121,13 +127,19 @@ func (s *socket) handleAction(conn net.Conn, handleCommand HandleCommand) {
|
||||
_ = sock.Close()
|
||||
}()
|
||||
|
||||
cmd, err := sock.Read()
|
||||
raw, err := sock.Read()
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to read command: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := handleCommand(cmd, sock); err != nil {
|
||||
cmd, args, err := parseCommand(raw)
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to parse command: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := handleCommand(cmd, args, sock); err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to handle command: %s", err))
|
||||
}
|
||||
}
|
||||
@@ -147,3 +159,21 @@ func canConnect(path string) bool {
|
||||
func isUseOfClosedNetworkError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "use of closed network connection")
|
||||
}
|
||||
|
||||
func parseCommand(raw string) (string, map[string]string, error) {
|
||||
var msg Message
|
||||
|
||||
if err := json.Unmarshal([]byte(raw), &msg); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if msg.Command == "" {
|
||||
return "", nil, errors.New("command is empty")
|
||||
}
|
||||
|
||||
if msg.Args == nil {
|
||||
msg.Args = map[string]string{}
|
||||
}
|
||||
|
||||
return msg.Command, msg.Args, nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,23 @@
|
||||
"notifications_queue_clear_error": "Failed to clear notification queue",
|
||||
"notifications_queue_clear_success": "The notification queue has been cleared.",
|
||||
|
||||
"cmd.daemon.block.Usage": "Blocking",
|
||||
"cmd.daemon.block.clear.Usage": "Unblock all banned IP addresses",
|
||||
"cmd.daemon.block.clear.Description": "Unblock all banned IP addresses.",
|
||||
"block_clear_error": "Unable to unblock all IP addresses",
|
||||
"block_clear_success": "The request was successfully completed",
|
||||
|
||||
"cmd.daemon.block.add.Usage": "Add IP address to block list",
|
||||
"cmd.daemon.block.add.Description": "Add an IP address to the block list. \nExamples: \nkor-elf-shield block add 192.168.1.1 \nkor-elf-shield block add 192.168.1.1 --seconds=600 \nkor-elf-shield block add 192.168.1.1 --port 80/tcp",
|
||||
"cmd.daemon.block.add.FlagUsage.port": "The port to be blocked. If not specified, all ports will be blocked. \nExamples: \n--port=80/tcp \n--port=1000/udp",
|
||||
"cmd.daemon.block.add.FlagUsage.seconds": "The blocking time in seconds. If not specified, the blocking will be permanent.",
|
||||
"cmd.daemon.block.add.FlagUsage.reason": "Reason for blocking.",
|
||||
"block_add_ip_success": "The IP address has been successfully added to the block list.",
|
||||
|
||||
"cmd.daemon.block.delete.Usage": "Remove IP address from block list",
|
||||
"cmd.daemon.block.delete.Description": "Remove an IP address from the block list. \nExample: \nkor-elf-shield block delete 192.168.1.1",
|
||||
"block_delete_ip_success": "The IP address has been successfully removed from the block list.",
|
||||
|
||||
"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}}",
|
||||
@@ -41,10 +58,21 @@
|
||||
"log": "Log: ",
|
||||
"user": "User",
|
||||
"access to user has been gained": "Access to user has been gained",
|
||||
"unknown": "unknown",
|
||||
"blockSec": "Blocked for {{.BlockSec}} seconds",
|
||||
"ports": "Ports: {{.Ports}}",
|
||||
|
||||
"alert.subject": "Alert detected ({{.Name}}) (group:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "Logged into the OS via ssh.",
|
||||
"alert.login.local.message": "Logged into the OS via TTY.",
|
||||
"alert.login.su.message": "Gained access to another user via su.",
|
||||
"alert.login.sudo.message": "Gained access to another user via sudo."
|
||||
"alert.login.sudo.message": "Gained access to another user via sudo.",
|
||||
|
||||
"alert.bruteForceProtection.subject": "A hacking attempt was detected and IP {{.IP}} was blocked. Alert ({{.Name}}) (Group:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.subject-error": "A hacking attempt was detected, but the IP {{.IP}} is not blocked. Alert ({{.Name}}) (group:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.error": "Error: {{.Error}}",
|
||||
"alert.bruteForceProtection.ssh.message": "An attempt to brute-force SSH was detected.",
|
||||
"alert.bruteForceProtection.group._default.message": "Default group.",
|
||||
|
||||
"cmd.error": "Command error: {{.Error}}"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,23 @@
|
||||
"notifications_queue_clear_error": "Хабарландыру кезегі тазаланбады",
|
||||
"notifications_queue_clear_success": "Хабарландыру кезегі тазартылды",
|
||||
|
||||
"cmd.daemon.block.Usage": "Бұғаттау",
|
||||
"cmd.daemon.block.clear.Usage": "Барлық тыйым салынған IP мекенжайларын бұғаттан шығарыңыз",
|
||||
"cmd.daemon.block.clear.Description": "Барлық тыйым салынған IP мекенжайларын бұғаттан шығарыңыз.",
|
||||
"block_clear_error": "Барлық IP мекенжайларын бұғаттан шығару мүмкін емес",
|
||||
"block_clear_success": "Сұраныс сәтті орындалды",
|
||||
|
||||
"cmd.daemon.block.add.Usage": "Блоктау тізіміне IP мекенжайын қосу",
|
||||
"cmd.daemon.block.add.Description": "Блоктау тізіміне IP мекенжайын қосыңыз. \nМысалдар: \nkor-elf-shield block add 192.168.1.1 \nkor-elf-shield block add 192.168.1.1 --seconds=600 \nkor-elf-shield block add 192.168.1.1 --port 80/tcp",
|
||||
"cmd.daemon.block.add.FlagUsage.port": "Блокталатын порт. Егер көрсетілмесе, барлық порттар бұғатталады. \nМысалдар: \n--port=80/tcp \n--port=1000/udp",
|
||||
"cmd.daemon.block.add.FlagUsage.seconds": "Блоктау уақыты секундпен. Егер көрсетілмесе, блоктау тұрақты болады.",
|
||||
"cmd.daemon.block.add.FlagUsage.reason": "Блоктау себебі.",
|
||||
"block_add_ip_success": "IP мекенжайы блоктау тізіміне сәтті қосылды.",
|
||||
|
||||
"cmd.daemon.block.delete.Usage": "IP мекенжайын блоктау тізімінен алып тастаңыз",
|
||||
"cmd.daemon.block.delete.Description": "IP мекенжайын блоктау тізімінен алып тастаңыз. \nМысал: \nkor-elf-shield block delete 192.168.1.1",
|
||||
"block_delete_ip_success": "IP мекенжайы блоктау тізімінен сәтті жойылды.",
|
||||
|
||||
"Command error": "Командалық қате",
|
||||
"invalid log level": "Параметрлерде көрсетілген журнал деңгейі жарамсыз. Ол қазір мына күйге орнатылған: {{.Level}}. Жарамды мәндер: {{.Levels}}",
|
||||
"invalid log encoding": "Жарамсыз кодтау параметрі. Қазіргі уақытта орнатылған: {{.Encoding}}. Жарамды мәндер: {{.Encodings}}",
|
||||
@@ -41,10 +58,21 @@
|
||||
"log": "Лог: ",
|
||||
"user": "Пайдаланушы",
|
||||
"access to user has been gained": "Пайдаланушыға кіру мүмкіндігі алынды",
|
||||
"unknown": "белгісіз",
|
||||
"blockSec": "{{.BlockSec}} секундқа блокталды",
|
||||
"ports": "Порттар: {{.Ports}}",
|
||||
|
||||
"alert.subject": "Ескерту анықталды ({{.Name}}) (топ:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "ОС-қа ssh арқылы кірді.",
|
||||
"alert.login.local.message": "ОЖ-ға TTY арқылы кірдіңіз.",
|
||||
"alert.login.su.message": "su арқылы басқа пайдаланушыға кіру мүмкіндігі алынды.",
|
||||
"alert.login.sudo.message": "sudo арқылы басқа пайдаланушыға кіру мүмкіндігі алынды."
|
||||
"alert.login.sudo.message": "sudo арқылы басқа пайдаланушыға кіру мүмкіндігі алынды.",
|
||||
|
||||
"alert.bruteForceProtection.subject": "Хакерлік әрекет анықталды және IP мекенжайы {{.IP}} бұғатталды. Ескерту ({{.Name}}) (Топ:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.subject-error": "Хакерлік әрекет анықталды, бірақ IP мекенжайы {{.IP}} бұғатталмаған. Ескерту ({{.Name}}) (Топ:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.error": "Қате: {{.Error}}",
|
||||
"alert.bruteForceProtection.ssh.message": "SSH-ті күштеп қолдану әрекеті анықталды.",
|
||||
"alert.bruteForceProtection.group._default.message": "Әдепкі топ.",
|
||||
|
||||
"cmd.error": "Команда қатесі: {{.Error}}"
|
||||
}
|
||||
@@ -25,6 +25,23 @@
|
||||
"notifications_queue_clear_error": "Не удалось очистить очередь уведомлений",
|
||||
"notifications_queue_clear_success": "Очередь уведомлений очищена",
|
||||
|
||||
"cmd.daemon.block.Usage": "Блокировка",
|
||||
"cmd.daemon.block.clear.Usage": "Разблокировать все забаненные IP адреса",
|
||||
"cmd.daemon.block.clear.Description": "блокировка все забаненные IP адреса.",
|
||||
"block_clear_error": "Не смогли разблокировать все IP адреса",
|
||||
"block_clear_success": "Запрос успешно выполнен",
|
||||
|
||||
"cmd.daemon.block.add.Usage": "Добавить IP адрес в список заблокированных",
|
||||
"cmd.daemon.block.add.Description": "Добавить IP адрес в список заблокированных. \nПримеры: \nkor-elf-shield block add 192.168.1.1 \nkor-elf-shield block add 192.168.1.1 --seconds=600 \nkor-elf-shield block add 192.168.1.1 --port 80/tcp",
|
||||
"cmd.daemon.block.add.FlagUsage.port": "Порт, который будет заблокирован. Если не указать, то заблокируются все порты. \nПримеры: \n--port=80/tcp \n--port=1000/udp",
|
||||
"cmd.daemon.block.add.FlagUsage.seconds": "Время блокировки в секундах. Если не указать, то блокировка будет вечной.",
|
||||
"cmd.daemon.block.add.FlagUsage.reason": "Причина блокировки.",
|
||||
"block_add_ip_success": "IP адрес успешно добавлен в список заблокированных.",
|
||||
|
||||
"cmd.daemon.block.delete.Usage": "Удалить IP адрес из списка заблокированных",
|
||||
"cmd.daemon.block.delete.Description": "Удалить IP адрес из списка заблокированных. \nПример: \nkor-elf-shield block delete 192.168.1.1",
|
||||
"block_delete_ip_success": "IP адрес успешно удален из списка заблокированных.",
|
||||
|
||||
"Command error": "Ошибка команды",
|
||||
"invalid log level": "В настройках указан не верный уровень log. Сейчас указан: {{.Level}}. Допустимые значения: {{.Levels}}",
|
||||
"invalid log encoding": "Неверная настройка encoding. Сейчас указан: {{.Encoding}}. Допустимые значения: {{.Encodings}}",
|
||||
@@ -41,10 +58,21 @@
|
||||
"log": "Лог: ",
|
||||
"user": "Пользователь",
|
||||
"access to user has been gained": "Получен доступ к пользователю",
|
||||
"unknown": "неизвестный",
|
||||
"blockSec": "Блокировка на {{.BlockSec}} секунд",
|
||||
"ports": "Порты: {{.Ports}}",
|
||||
|
||||
"alert.subject": "Обнаружено оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "Вошли в ОС через ssh.",
|
||||
"alert.login.local.message": "Вошли в ОС через TTY.",
|
||||
"alert.login.su.message": "Получили доступ к другому пользователю через su.",
|
||||
"alert.login.sudo.message": "Получили доступ к другому пользователю через sudo."
|
||||
"alert.login.sudo.message": "Получили доступ к другому пользователю через sudo.",
|
||||
|
||||
"alert.bruteForceProtection.subject": "Обнаружена попытка взлома, IP {{.IP}} заблокирован. Оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.subject-error": "Обнаружена попытка взлома, но IP {{.IP}} не заблокирован. Оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||
"alert.bruteForceProtection.error": "Ошибка: {{.Error}}",
|
||||
"alert.bruteForceProtection.ssh.message": "Обнаружена попытка атаки на SSH методом перебора паролей.",
|
||||
"alert.bruteForceProtection.group._default.message": "Группа по умолчанию.",
|
||||
|
||||
"cmd.error": "Ошибка команды: {{.Error}}"
|
||||
}
|
||||
@@ -4,40 +4,40 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"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/types"
|
||||
)
|
||||
|
||||
func ToDirection(direction string) (firewall.Direction, error) {
|
||||
func ToDirection(direction string) (types.Direction, error) {
|
||||
switch strings.ToLower(direction) {
|
||||
case "in":
|
||||
return firewall.DirectionIn, nil
|
||||
return types.DirectionIn, nil
|
||||
case "out":
|
||||
return firewall.DirectionOut, nil
|
||||
return types.DirectionOut, nil
|
||||
default:
|
||||
return firewall.DirectionIn, errors.New("invalid direction. Must be in or out")
|
||||
return types.DirectionIn, errors.New("invalid direction. Must be in or out")
|
||||
}
|
||||
}
|
||||
|
||||
func ToProtocol(protocol string) (firewall.Protocol, error) {
|
||||
func ToProtocol(protocol string) (types.Protocol, error) {
|
||||
switch strings.ToLower(protocol) {
|
||||
case "tcp":
|
||||
return firewall.ProtocolTCP, nil
|
||||
return types.ProtocolTCP, nil
|
||||
case "udp":
|
||||
return firewall.ProtocolUDP, nil
|
||||
return types.ProtocolUDP, nil
|
||||
default:
|
||||
return firewall.ProtocolTCP, errors.New("invalid protocol. Must be tcp or udp")
|
||||
return types.ProtocolTCP, errors.New("invalid protocol. Must be tcp or udp")
|
||||
}
|
||||
}
|
||||
|
||||
func ToAction(action string) (firewall.Action, error) {
|
||||
func ToAction(action string) (types.Action, error) {
|
||||
switch strings.ToLower(action) {
|
||||
case "accept":
|
||||
return firewall.ActionAccept, nil
|
||||
return types.ActionAccept, nil
|
||||
case "drop":
|
||||
return firewall.ActionDrop, nil
|
||||
return types.ActionDrop, nil
|
||||
case "reject":
|
||||
return firewall.ActionReject, nil
|
||||
return types.ActionReject, nil
|
||||
default:
|
||||
return firewall.ActionAccept, errors.New("invalid action. Must be accept, drop or reject")
|
||||
return types.ActionAccept, errors.New("invalid action. Must be accept, drop or reject")
|
||||
}
|
||||
}
|
||||
|
||||
25
internal/pkg/regular_expression/regexp.go
Normal file
25
internal/pkg/regular_expression/regexp.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package regular_expression
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type LazyRegexp struct {
|
||||
pattern string
|
||||
|
||||
once sync.Once
|
||||
re *regexp.Regexp
|
||||
err error
|
||||
}
|
||||
|
||||
func NewLazyRegexp(pattern string) *LazyRegexp {
|
||||
return &LazyRegexp{pattern: pattern}
|
||||
}
|
||||
|
||||
func (lr *LazyRegexp) Get() (*regexp.Regexp, error) {
|
||||
lr.once.Do(func() {
|
||||
lr.re, lr.err = regexp.Compile(lr.pattern)
|
||||
})
|
||||
return lr.re, lr.err
|
||||
}
|
||||
23
internal/pkg/time_operation/time.go
Normal file
23
internal/pkg/time_operation/time.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package time_operation
|
||||
|
||||
import "time"
|
||||
|
||||
func IsRateLimited(lastTriggeredAtUnix int64, eventTime time.Time, rateLimit int64) bool {
|
||||
if lastTriggeredAtUnix == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return eventTime.Unix()-lastTriggeredAtUnix < rateLimit
|
||||
}
|
||||
|
||||
func IsReset(lastTriggeredAtUnix int64, eventTime time.Time, resetPeriod int64) bool {
|
||||
if resetPeriod == 0 || lastTriggeredAtUnix == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if eventTime.Unix()-lastTriggeredAtUnix > resetPeriod {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Login Login
|
||||
Login Login
|
||||
LogAlert LogAlert
|
||||
BruteForceProtection BruteForceProtection
|
||||
}
|
||||
|
||||
func InitSetting(path string) (Setting, error) {
|
||||
@@ -33,7 +35,9 @@ func InitSetting(path string) (Setting, error) {
|
||||
|
||||
func settingDefault() Setting {
|
||||
return Setting{
|
||||
Login: defaultLogin(),
|
||||
Login: defaultLogin(),
|
||||
LogAlert: defaultLogAlert(),
|
||||
BruteForceProtection: defaultBruteForceProtection(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +50,18 @@ func (s Setting) ToSources() ([]*config.Source, error) {
|
||||
}
|
||||
sources = append(sources, loginSources...)
|
||||
|
||||
alertSources, err := s.LogAlert.ToSources()
|
||||
if err != nil {
|
||||
return sources, err
|
||||
}
|
||||
sources = append(sources, alertSources...)
|
||||
|
||||
bruteForceSources, err := s.BruteForceProtection.ToSources()
|
||||
if err != nil {
|
||||
return sources, err
|
||||
}
|
||||
sources = append(sources, bruteForceSources...)
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
@@ -53,6 +69,12 @@ func (s Setting) Validate() error {
|
||||
if err := s.Login.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.LogAlert.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.BruteForceProtection.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
199
internal/setting/analyzer/brute_force_protection.go
Normal file
199
internal/setting/analyzer/brute_force_protection.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/analyzer/config/brute_force_protection"
|
||||
"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/i18n"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
type BruteForceProtection struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
RateLimitCount int `mapstructure:"rate_limit_count"`
|
||||
RateLimitPeriod int `mapstructure:"rate_limit_period"`
|
||||
RateLimitResetPeriod int `mapstructure:"rate_limit_reset_period"`
|
||||
BlockingTime int `mapstructure:"blocking_time"`
|
||||
SSHEnable bool `mapstructure:"ssh_enable"`
|
||||
SSHNotify bool `mapstructure:"ssh_notify"`
|
||||
SSHGroup string `mapstructure:"ssh_group"`
|
||||
|
||||
Groups []BruteForceProtectionGroup
|
||||
Rules []BruteForceProtectionRule
|
||||
}
|
||||
|
||||
func defaultBruteForceProtection() BruteForceProtection {
|
||||
return BruteForceProtection{
|
||||
Enabled: true,
|
||||
Notify: true,
|
||||
RateLimitCount: 5,
|
||||
RateLimitPeriod: 3600,
|
||||
RateLimitResetPeriod: 86400,
|
||||
BlockingTime: 3600,
|
||||
SSHEnable: true,
|
||||
SSHNotify: true,
|
||||
SSHGroup: "",
|
||||
|
||||
Groups: []BruteForceProtectionGroup{},
|
||||
Rules: []BruteForceProtectionRule{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BruteForceProtection) Validate() error {
|
||||
if p.RateLimitPeriod <= 0 {
|
||||
return errors.New("rate limit period must be greater than 0")
|
||||
}
|
||||
|
||||
if p.RateLimitCount <= 0 {
|
||||
return errors.New("rate limit count must be greater than 0")
|
||||
}
|
||||
|
||||
if p.RateLimitResetPeriod < 0 {
|
||||
return errors.New("rate limit reset period must be positive")
|
||||
}
|
||||
|
||||
if p.BlockingTime < 0 {
|
||||
return errors.New("blocking time must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *BruteForceProtection) ToSources() ([]*config.Source, error) {
|
||||
var sources []*config.Source
|
||||
|
||||
if !p.Enabled {
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
groups, err := p.groups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.SSHEnable {
|
||||
sshGroup := "_default"
|
||||
if p.SSHGroup != "" {
|
||||
if _, ok := groups[p.SSHGroup]; !ok {
|
||||
return nil, errors.New("ssh group not found")
|
||||
}
|
||||
sshGroup = p.SSHGroup
|
||||
}
|
||||
sshSources, err := config.NewBruteForceProtectionSSH(p.Notify && p.SSHNotify, groups[sshGroup])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, sshSources...)
|
||||
}
|
||||
|
||||
for _, rule := range p.Rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
var group *brute_force_protection.Group
|
||||
groupName := "_default"
|
||||
if rule.Group != "" {
|
||||
groupName = rule.Group
|
||||
}
|
||||
if _, ok := groups[groupName]; !ok {
|
||||
return nil, fmt.Errorf("group %q not found", rule.Group)
|
||||
}
|
||||
group = groups[groupName]
|
||||
|
||||
source, err := rule.ToSource(p.Notify, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func (p *BruteForceProtection) groups() (map[string]*brute_force_protection.Group, error) {
|
||||
groups := make(map[string]*brute_force_protection.Group)
|
||||
|
||||
groups["_default"] = &brute_force_protection.Group{
|
||||
Name: "_default",
|
||||
Message: i18n.Lang.T("alert.bruteForceProtection.group._default.message"),
|
||||
RateLimits: []brute_force_protection.RateLimit{
|
||||
{
|
||||
Count: uint32(p.RateLimitCount),
|
||||
Period: uint32(p.RateLimitPeriod),
|
||||
BlockingTimeSeconds: uint32(p.BlockingTime),
|
||||
BlockConfig: brute_force_protection.NewBlockOnceIPConfig(),
|
||||
},
|
||||
},
|
||||
RateLimitResetPeriod: uint32(p.RateLimitResetPeriod),
|
||||
}
|
||||
|
||||
for _, group := range p.Groups {
|
||||
g, err := group.ToGroup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups[g.Name] = g
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func toBlockConfigBySettings(blockType string, ports []string) (brute_force_protection.Block, error) {
|
||||
if blockType == "" {
|
||||
return nil, errors.New("block type is empty")
|
||||
}
|
||||
|
||||
switch blockType {
|
||||
case "ip":
|
||||
return brute_force_protection.NewBlockOnceIPConfig(), nil
|
||||
case "ip_port":
|
||||
if len(ports) == 0 {
|
||||
return nil, errors.New("ports is empty")
|
||||
}
|
||||
|
||||
var blockPorts []types.L4Port
|
||||
for _, port := range ports {
|
||||
l4Port, err := toL4Port(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockPorts = append(blockPorts, l4Port)
|
||||
}
|
||||
|
||||
return brute_force_protection.NewBlockIPAndPortsConfig(blockPorts), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unknown block type")
|
||||
}
|
||||
|
||||
func toL4Port(portString string) (types.L4Port, error) {
|
||||
if portString == "" {
|
||||
return nil, errors.New("port is empty")
|
||||
}
|
||||
|
||||
data := strings.Split(portString, "/")
|
||||
protocol := types.ProtocolTCP
|
||||
port, err := strconv.Atoi(data[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validate.Port(port, "port"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) == 2 {
|
||||
protocol, err = ip.ToProtocol(data[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return types.NewL4Port(uint16(port), protocol)
|
||||
}
|
||||
68
internal/setting/analyzer/brute_force_protection_group.go
Normal file
68
internal/setting/analyzer/brute_force_protection_group.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
type BruteForceProtectionGroup struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Message string `mapstructure:"message"`
|
||||
RateLimitResetPeriod int `mapstructure:"rate_limit_reset_period"`
|
||||
BlockType string `mapstructure:"block_type"`
|
||||
Ports []string `mapstructure:"ports"`
|
||||
RateLimits []BruteForceProtectionGroupRateLimit `mapstructure:"rate_limits"`
|
||||
}
|
||||
|
||||
func (g *BruteForceProtectionGroup) ToGroup() (*brute_force_protection.Group, error) {
|
||||
if err := g.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rateLimits []brute_force_protection.RateLimit
|
||||
|
||||
blockType := g.BlockType
|
||||
if blockType == "" {
|
||||
blockType = "ip"
|
||||
}
|
||||
blockConfig, err := toBlockConfigBySettings(blockType, g.Ports)
|
||||
if err := err; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rateLimit := range g.RateLimits {
|
||||
rLimit, err := rateLimit.ToRateLimit(blockConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rateLimits = append(rateLimits, rLimit)
|
||||
}
|
||||
|
||||
return &brute_force_protection.Group{
|
||||
Name: g.Name,
|
||||
Message: g.Message,
|
||||
RateLimits: rateLimits,
|
||||
RateLimitResetPeriod: uint32(g.RateLimitResetPeriod),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *BruteForceProtectionGroup) validate() error {
|
||||
if g.Name == "" {
|
||||
return fmt.Errorf("brute force protection group name is empty")
|
||||
}
|
||||
|
||||
if !reName.MatchString(g.Name) {
|
||||
return fmt.Errorf("brute force protection group invalid name: %s", g.Name)
|
||||
}
|
||||
|
||||
if g.RateLimitResetPeriod < 0 {
|
||||
return fmt.Errorf("brute force protection group rate limit reset period must be positive")
|
||||
}
|
||||
|
||||
if len(g.RateLimits) == 0 {
|
||||
return fmt.Errorf("brute force protection group rate limits is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
type BruteForceProtectionGroupRateLimit struct {
|
||||
Count int `mapstructure:"count"`
|
||||
Period int `mapstructure:"period"`
|
||||
BlockingTime int `mapstructure:"blocking_time"`
|
||||
BlockType string `mapstructure:"block_type"`
|
||||
Ports []string `mapstructure:"ports"`
|
||||
}
|
||||
|
||||
func (l *BruteForceProtectionGroupRateLimit) ToRateLimit(blockConfig brute_force_protection.Block) (brute_force_protection.RateLimit, error) {
|
||||
if err := l.validate(); err != nil {
|
||||
return brute_force_protection.RateLimit{}, err
|
||||
}
|
||||
|
||||
var rateLimitBlockConfig brute_force_protection.Block
|
||||
|
||||
if l.BlockType != "" {
|
||||
var err error
|
||||
rateLimitBlockConfig, err = toBlockConfigBySettings(l.BlockType, l.Ports)
|
||||
if err != nil {
|
||||
return brute_force_protection.RateLimit{}, err
|
||||
}
|
||||
} else {
|
||||
rateLimitBlockConfig = blockConfig
|
||||
}
|
||||
|
||||
return brute_force_protection.RateLimit{
|
||||
Count: uint32(l.Count),
|
||||
Period: uint32(l.Period),
|
||||
BlockingTimeSeconds: uint32(l.BlockingTime),
|
||||
BlockConfig: rateLimitBlockConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *BruteForceProtectionGroupRateLimit) validate() error {
|
||||
if l.Count <= 0 {
|
||||
return fmt.Errorf("count must be greater than 0")
|
||||
}
|
||||
if l.Period <= 0 {
|
||||
return fmt.Errorf("period must be greater than 0")
|
||||
}
|
||||
if l.BlockingTime < 0 {
|
||||
return fmt.Errorf("blocking time must be non-negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
48
internal/setting/analyzer/brute_force_protection_pattern.go
Normal file
48
internal/setting/analyzer/brute_force_protection_pattern.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"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/pkg/regular_expression"
|
||||
)
|
||||
|
||||
type BruteForceProtectionPattern struct {
|
||||
Regexp string `mapstructure:"regexp"`
|
||||
IP int `mapstructure:"ip"`
|
||||
Values []PatternValue
|
||||
}
|
||||
|
||||
func (p *BruteForceProtectionPattern) ToPattern() (brute_force_protection.RegexPattern, error) {
|
||||
if err := p.validate(); err != nil {
|
||||
return brute_force_protection.RegexPattern{}, err
|
||||
}
|
||||
|
||||
pattern := brute_force_protection.RegexPattern{
|
||||
Regexp: regular_expression.NewLazyRegexp(p.Regexp),
|
||||
IP: uint8(p.IP),
|
||||
}
|
||||
|
||||
for _, value := range p.Values {
|
||||
v, err := value.ToPatternValueForBruteForceProtection()
|
||||
if err != nil {
|
||||
return brute_force_protection.RegexPattern{}, err
|
||||
}
|
||||
|
||||
pattern.Values = append(pattern.Values, v)
|
||||
}
|
||||
|
||||
return pattern, nil
|
||||
}
|
||||
|
||||
func (p *BruteForceProtectionPattern) validate() error {
|
||||
if p.IP <= 0 || p.IP > 255 {
|
||||
return errors.New("ip must be between 1 and 255")
|
||||
}
|
||||
|
||||
if p.Regexp == "" {
|
||||
return errors.New("regexp is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
69
internal/setting/analyzer/brute_force_protection_rule.go
Normal file
69
internal/setting/analyzer/brute_force_protection_rule.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (l *BruteForceProtectionRule) ToSource(isNotify bool, group *brute_force_protection.Group) (*config.Source, error) {
|
||||
if err := l.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if group == nil {
|
||||
return nil, fmt.Errorf("brute force protection group is empty")
|
||||
}
|
||||
|
||||
source, err := l.Source.ToSource()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var patterns []brute_force_protection.RegexPattern
|
||||
|
||||
for _, pattern := range l.Patterns {
|
||||
p, err := pattern.ToPattern()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patterns = append(patterns, p)
|
||||
}
|
||||
|
||||
if len(patterns) == 0 {
|
||||
return nil, fmt.Errorf("patterns is empty")
|
||||
}
|
||||
|
||||
source.BruteForceProtectionRule = &brute_force_protection.Rule{
|
||||
Name: l.Name,
|
||||
Message: l.Message,
|
||||
IsNotification: isNotify && l.Notify,
|
||||
Patterns: patterns,
|
||||
Group: group,
|
||||
}
|
||||
|
||||
return source, nil
|
||||
}
|
||||
|
||||
func (l *BruteForceProtectionRule) validate() error {
|
||||
if l.Name == "" {
|
||||
return fmt.Errorf("brute force protection name is empty")
|
||||
}
|
||||
|
||||
if !reName.MatchString(l.Name) {
|
||||
return fmt.Errorf("brute force protection invalid name: %s", l.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
80
internal/setting/analyzer/log_alert.go
Normal file
80
internal/setting/analyzer/log_alert.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
var (
|
||||
reName = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]{0,254}$`)
|
||||
)
|
||||
|
||||
type LogAlert struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
Groups []LogAlertGroup
|
||||
Rules []LogAlertRule
|
||||
}
|
||||
|
||||
func defaultLogAlert() LogAlert {
|
||||
return LogAlert{
|
||||
Enabled: true,
|
||||
Notify: true,
|
||||
Groups: []LogAlertGroup{},
|
||||
Rules: []LogAlertRule{},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LogAlert) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LogAlert) ToSources() ([]*config.Source, error) {
|
||||
var sources []*config.Source
|
||||
|
||||
if !l.Enabled || len(l.Rules) == 0 {
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
groups, err := l.groups()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("groups: %w", err)
|
||||
}
|
||||
|
||||
for _, rule := range l.Rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
var group *config.AlertGroup
|
||||
if rule.Group != "" {
|
||||
if _, ok := groups[rule.Group]; !ok {
|
||||
return nil, fmt.Errorf("group %q not found", rule.Group)
|
||||
}
|
||||
group = groups[rule.Group]
|
||||
}
|
||||
|
||||
source, err := rule.ToSource(l.Notify, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func (l *LogAlert) groups() (map[string]*config.AlertGroup, error) {
|
||||
groups := make(map[string]*config.AlertGroup)
|
||||
for _, group := range l.Groups {
|
||||
g, err := group.ToGroup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups[g.Name] = g
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
57
internal/setting/analyzer/log_alert_group.go
Normal file
57
internal/setting/analyzer/log_alert_group.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
type LogAlertGroup struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Message string `mapstructure:"message"`
|
||||
RateLimitResetPeriod int `mapstructure:"rate_limit_reset_period"`
|
||||
RateLimits []LogAlertGroupRateLimit `mapstructure:"rate_limits"`
|
||||
}
|
||||
|
||||
func (g *LogAlertGroup) ToGroup() (*config.AlertGroup, error) {
|
||||
if err := g.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rateLimits []config.RateLimit
|
||||
|
||||
for _, rateLimit := range g.RateLimits {
|
||||
rLimit, err := rateLimit.ToRateLimit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rateLimits = append(rateLimits, rLimit)
|
||||
}
|
||||
|
||||
return &config.AlertGroup{
|
||||
Name: g.Name,
|
||||
Message: g.Message,
|
||||
RateLimits: rateLimits,
|
||||
RateLimitResetPeriod: uint32(g.RateLimitResetPeriod),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *LogAlertGroup) validate() error {
|
||||
if g.Name == "" {
|
||||
return fmt.Errorf("alert group name is empty")
|
||||
}
|
||||
|
||||
if !reName.MatchString(g.Name) {
|
||||
return fmt.Errorf("alert group invalid name: %s", g.Name)
|
||||
}
|
||||
|
||||
if g.RateLimitResetPeriod < 0 {
|
||||
return fmt.Errorf("alert group rate limit reset period must be positive")
|
||||
}
|
||||
|
||||
if len(g.RateLimits) == 0 {
|
||||
return fmt.Errorf("alert group rate limits is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
33
internal/setting/analyzer/log_alert_group_rate_limit.go
Normal file
33
internal/setting/analyzer/log_alert_group_rate_limit.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
type LogAlertGroupRateLimit struct {
|
||||
Count int `mapstructure:"count"`
|
||||
Period int `mapstructure:"period"`
|
||||
}
|
||||
|
||||
func (l *LogAlertGroupRateLimit) ToRateLimit() (config.RateLimit, error) {
|
||||
if err := l.validate(); err != nil {
|
||||
return config.RateLimit{}, err
|
||||
}
|
||||
|
||||
return config.RateLimit{
|
||||
Count: uint32(l.Count),
|
||||
Period: uint32(l.Period),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LogAlertGroupRateLimit) validate() error {
|
||||
if l.Count <= 0 {
|
||||
return fmt.Errorf("count must be greater than 0")
|
||||
}
|
||||
if l.Period <= 0 {
|
||||
return fmt.Errorf("period must be greater than 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
28
internal/setting/analyzer/log_alert_pattern.go
Normal file
28
internal/setting/analyzer/log_alert_pattern.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"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/pkg/regular_expression"
|
||||
)
|
||||
|
||||
type LogAlertPattern struct {
|
||||
Regexp string `mapstructure:"regexp"`
|
||||
Values []PatternValue
|
||||
}
|
||||
|
||||
func (p *LogAlertPattern) ToPattern() (config.AlertRegexPattern, error) {
|
||||
pattern := config.AlertRegexPattern{
|
||||
Regexp: regular_expression.NewLazyRegexp(p.Regexp),
|
||||
}
|
||||
|
||||
for _, value := range p.Values {
|
||||
v, err := value.ToPatternValue()
|
||||
if err != nil {
|
||||
return config.AlertRegexPattern{}, err
|
||||
}
|
||||
|
||||
pattern.Values = append(pattern.Values, v)
|
||||
}
|
||||
|
||||
return pattern, nil
|
||||
}
|
||||
66
internal/setting/analyzer/log_alert_rule.go
Normal file
66
internal/setting/analyzer/log_alert_rule.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
type LogAlertRule struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
Name string `mapstructure:"name"`
|
||||
Message string `mapstructure:"message"`
|
||||
Group string `mapstructure:"group"`
|
||||
Source Source
|
||||
Patterns []LogAlertPattern
|
||||
}
|
||||
|
||||
func (l *LogAlertRule) ToSource(isNotify bool, group *config.AlertGroup) (*config.Source, error) {
|
||||
if err := l.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source, err := l.Source.ToSource()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var patterns []config.AlertRegexPattern
|
||||
|
||||
for _, pattern := range l.Patterns {
|
||||
p, err := pattern.ToPattern()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patterns = append(patterns, p)
|
||||
}
|
||||
|
||||
if len(patterns) == 0 {
|
||||
return nil, fmt.Errorf("patterns is empty")
|
||||
}
|
||||
|
||||
source.AlertRule = &config.AlertRule{
|
||||
Name: l.Name,
|
||||
Message: l.Message,
|
||||
IsNotification: isNotify && l.Notify,
|
||||
Patterns: patterns,
|
||||
}
|
||||
if group != nil {
|
||||
source.AlertRule.Group = group
|
||||
}
|
||||
|
||||
return source, nil
|
||||
}
|
||||
|
||||
func (l *LogAlertRule) validate() error {
|
||||
if l.Name == "" {
|
||||
return fmt.Errorf("alert name is empty")
|
||||
}
|
||||
|
||||
if !reName.MatchString(l.Name) {
|
||||
return fmt.Errorf("alert invalid name: %s", l.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -52,19 +52,35 @@ func (l Login) ToSources() ([]*config.Source, error) {
|
||||
}
|
||||
|
||||
if l.SSHEnable {
|
||||
sources = append(sources, config.NewLoginSSH(l.Notify && l.SSHNotify)...)
|
||||
loginSources, err := config.NewLoginSSH(l.Notify && l.SSHNotify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, loginSources...)
|
||||
}
|
||||
|
||||
if l.LocalEnable {
|
||||
sources = append(sources, config.NewLoginLocal(l.Notify && l.LocalNotify)...)
|
||||
loginSources, err := config.NewLoginLocal(l.Notify && l.LocalNotify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, loginSources...)
|
||||
}
|
||||
|
||||
if l.SuEnable {
|
||||
sources = append(sources, config.NewLoginSu(l.Notify && l.SuNotify)...)
|
||||
loginSources, err := config.NewLoginSu(l.Notify && l.SuNotify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, loginSources...)
|
||||
}
|
||||
|
||||
if l.SudoEnable {
|
||||
sources = append(sources, config.NewLoginSudo(l.Notify && l.SudoNotify)...)
|
||||
loginSources, err := config.NewLoginSudo(l.Notify && l.SudoNotify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, loginSources...)
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
|
||||
51
internal/setting/analyzer/pattern_value.go
Normal file
51
internal/setting/analyzer/pattern_value.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/analyzer/config/brute_force_protection"
|
||||
)
|
||||
|
||||
type PatternValue struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Value int8 `mapstructure:"value"`
|
||||
}
|
||||
|
||||
func (v *PatternValue) ToPatternValue() (config.PatternValue, error) {
|
||||
if err := v.validate(); err != nil {
|
||||
return config.PatternValue{}, err
|
||||
}
|
||||
|
||||
value := config.PatternValue{
|
||||
Name: v.Name,
|
||||
Value: uint8(v.Value),
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (v *PatternValue) ToPatternValueForBruteForceProtection() (brute_force_protection.PatternValue, error) {
|
||||
if err := v.validate(); err != nil {
|
||||
return brute_force_protection.PatternValue{}, err
|
||||
}
|
||||
|
||||
value := brute_force_protection.PatternValue{
|
||||
Name: v.Name,
|
||||
Value: uint8(v.Value),
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (v *PatternValue) validate() error {
|
||||
if v.Value <= 0 {
|
||||
return fmt.Errorf("value must be greater than 0")
|
||||
}
|
||||
|
||||
if v.Name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
59
internal/setting/analyzer/source.go
Normal file
59
internal/setting/analyzer/source.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Field string `mapstructure:"field"`
|
||||
Match string `mapstructure:"match"`
|
||||
Path string `mapstructure:"path"`
|
||||
}
|
||||
|
||||
func (s *Source) ToSource() (*config.Source, error) {
|
||||
switch s.Type {
|
||||
case "journalctl":
|
||||
field, err := s.journalField()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
journal, err := config.NewSourceJournal(field, s.Match)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config.Source{
|
||||
Type: config.SourceTypeJournal,
|
||||
Journal: journal,
|
||||
}, nil
|
||||
case "file":
|
||||
file, err := config.NewSourceFile(s.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config.Source{
|
||||
Type: config.SourceTypeFile,
|
||||
File: file,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New(fmt.Sprintf("unknown source type: %s. journalctl or file are allowed.", s.Type))
|
||||
}
|
||||
|
||||
func (s *Source) journalField() (config.JournalField, error) {
|
||||
switch strings.ToLower(s.Field) {
|
||||
case "systemd_unit":
|
||||
return config.JournalFieldSystemdUnit, nil
|
||||
case "syslog_identifier":
|
||||
return config.JournalFieldSyslogIdentifier, nil
|
||||
}
|
||||
|
||||
return "", errors.New(fmt.Sprintf("unknown journal field: %s. systemd_unit or syslog_identifier are allowed.", s.Field))
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
|
||||
"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/types"
|
||||
port2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
@@ -99,7 +100,7 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
if len(ports) == 0 {
|
||||
// If no port is specified, we only allow the IP address to be accepted.
|
||||
addIP.OnlyIP = true
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
in = append(in, addIP)
|
||||
} else {
|
||||
out = append(out, addIP)
|
||||
@@ -108,13 +109,12 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
}
|
||||
|
||||
if len(protocols) == 0 {
|
||||
addIP.Protocol = firewall.ProtocolTCP
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, addDirection)
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, addDirection, types.ProtocolTCP)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
in = append(in, addIn...)
|
||||
} else {
|
||||
out = append(out, addOut...)
|
||||
@@ -127,7 +127,7 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
error = err
|
||||
return
|
||||
}
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
in = append(in, addIn...)
|
||||
} else {
|
||||
out = append(out, addOut...)
|
||||
@@ -136,7 +136,7 @@ func loopIP(baseConfigIP firewall.ConfigIP, directions []string, protocols []str
|
||||
return
|
||||
}
|
||||
|
||||
func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []int, direction firewall.Direction) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []int, direction types.Direction) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
for _, protocol := range protocols {
|
||||
addProtocol, err := port2.ToProtocol(protocol)
|
||||
if err != nil {
|
||||
@@ -144,10 +144,9 @@ func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []
|
||||
return
|
||||
}
|
||||
addIP := baseConfigIP
|
||||
addIP.Protocol = addProtocol
|
||||
|
||||
if len(ports) == 0 {
|
||||
if direction == firewall.DirectionIn {
|
||||
if direction == types.DirectionIn {
|
||||
in = append(in, addIP)
|
||||
} else {
|
||||
out = append(out, addIP)
|
||||
@@ -155,12 +154,12 @@ func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []
|
||||
continue
|
||||
}
|
||||
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, direction)
|
||||
addIn, addOut, err := loopIPPort(addIP, ports, direction, addProtocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
if direction == firewall.DirectionIn {
|
||||
if direction == types.DirectionIn {
|
||||
in = append(in, addIn...)
|
||||
} else {
|
||||
out = append(out, addOut...)
|
||||
@@ -170,15 +169,22 @@ func loopIPProtocol(baseConfigIP firewall.ConfigIP, protocols []string, ports []
|
||||
return
|
||||
}
|
||||
|
||||
func loopIPPort(baseConfigIP firewall.ConfigIP, ports []int, direction firewall.Direction) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
func loopIPPort(baseConfigIP firewall.ConfigIP, ports []int, direction types.Direction, protocol types.Protocol) (in []firewall.ConfigIP, out []firewall.ConfigIP, error error) {
|
||||
for _, port := range ports {
|
||||
if err := validate.Port(port, "port"); err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
l4Port, err := types.NewL4Port(uint16(port), protocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
addIP := baseConfigIP
|
||||
addIP.Port = uint16(port)
|
||||
if direction == firewall.DirectionIn {
|
||||
addIP.Port = l4Port
|
||||
if direction == types.DirectionIn {
|
||||
in = append(in, addIP)
|
||||
} else {
|
||||
out = append(out, addIP)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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/types"
|
||||
)
|
||||
|
||||
type policy struct {
|
||||
@@ -61,15 +62,15 @@ func (p policy) ToConfigPolicy() (firewall.ConfigPolicy, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p policy) dropToPolicyDrop(drop string, parametrName string) (firewall.PolicyDrop, error) {
|
||||
func (p policy) dropToPolicyDrop(drop string, parametrName string) (types.PolicyDrop, error) {
|
||||
if drop == "" {
|
||||
return 0, fmt.Errorf("%s is empty", parametrName)
|
||||
}
|
||||
switch drop {
|
||||
case "drop":
|
||||
return firewall.Drop, nil
|
||||
return types.Drop, nil
|
||||
case "reject":
|
||||
return firewall.Reject, nil
|
||||
return types.Reject, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid %s . Must be drop or reject", parametrName)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"errors"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall"
|
||||
port2 "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/ip"
|
||||
"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/pkg/ip"
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||
)
|
||||
|
||||
@@ -25,7 +26,7 @@ func (p *Port) ToPorts() (InPorts []firewall.ConfigPort, OutPorts []firewall.Con
|
||||
error = err
|
||||
return
|
||||
}
|
||||
action, err := port2.ToAction(p.Action)
|
||||
action, err := ip.ToAction(p.Action)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
@@ -37,25 +38,30 @@ func (p *Port) ToPorts() (InPorts []firewall.ConfigPort, OutPorts []firewall.Con
|
||||
return
|
||||
}
|
||||
for _, direction := range p.Directions {
|
||||
addDirection, err := port2.ToDirection(direction)
|
||||
addDirection, err := ip.ToDirection(direction)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
for _, protocol := range p.Protocols {
|
||||
addProtocol, err := port2.ToProtocol(protocol)
|
||||
addProtocol, err := ip.ToProtocol(protocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
l4Port, err := types.NewL4Port(uint16(port), addProtocol)
|
||||
if err != nil {
|
||||
error = err
|
||||
return
|
||||
}
|
||||
|
||||
addPort := firewall.ConfigPort{
|
||||
Number: uint16(port),
|
||||
Protocol: addProtocol,
|
||||
Port: l4Port,
|
||||
Action: action,
|
||||
LimitRate: p.LimitRate,
|
||||
}
|
||||
if addDirection == firewall.DirectionIn {
|
||||
if addDirection == types.DirectionIn {
|
||||
InPorts = append(InPorts, addPort)
|
||||
} else {
|
||||
OutPorts = append(OutPorts, addPort)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package socket
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/socket"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
Send(command string) (result string, err error)
|
||||
SendCommand(command string, args map[string]string) (result string, err error)
|
||||
Read() (string, error)
|
||||
Close() error
|
||||
}
|
||||
@@ -21,7 +27,35 @@ func NewSocketClient(path string) (Client, error) {
|
||||
}
|
||||
|
||||
func (s *client) Send(command string) (result string, err error) {
|
||||
_, err = s.conn.Write([]byte(command))
|
||||
msg := socket.Message{
|
||||
Command: command,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = s.conn.Write(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return s.Read()
|
||||
}
|
||||
|
||||
func (s *client) SendCommand(command string, args map[string]string) (string, error) {
|
||||
msg := socket.Message{
|
||||
Command: command,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = s.conn.Write(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user