Compare commits
11 Commits
v0.5.0
...
0fa8d88479
| Author | SHA1 | Date | |
|---|---|---|---|
|
0fa8d88479
|
|||
|
9eef81d1a5
|
|||
|
6821924c8e
|
|||
|
f0958a340f
|
|||
|
d9a40c620c
|
|||
|
fd764fb5c5
|
|||
|
d6af8a7ea5
|
|||
|
f0d5b597cb
|
|||
|
81a28bf485
|
|||
|
0fb8c0b42d
|
|||
|
6b79928b3a
|
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,3 +1,25 @@
|
|||||||
|
## 0.6.0 (soon)
|
||||||
|
***
|
||||||
|
#### Русский
|
||||||
|
* Добавлена возможность повторной отправки уведомления, если в прошлый раз произошла ошибка.
|
||||||
|
* Добавлена команда `kor-elf-shield notifications queue count`, которая возвращает количество уведомлений в очереди в базе данных.
|
||||||
|
* Добавлена команда `kor-elf-shield notifications queue clear`, которая удаляет все уведомления из очереди в базе данных.
|
||||||
|
* В файл настроек kor-elf-shield.toml добавлены новые параметры:
|
||||||
|
* data_dir = Каталог для постоянных данных приложения (state): локальная база данных, кэш/индексы, файлы состояния и другие служебные файлы. Должен быть доступен на запись пользователю, от имени которого запущен демон. Если каталог не существует — будет создан. По умолчанию: "/var/lib/kor-elf-shield/"
|
||||||
|
* В файл настроек notifications.toml добавлены новые параметры:
|
||||||
|
* enable_retries = Включает повторные попытки отправить уведомление, если сразу не получилось. По умолчанию: true
|
||||||
|
* retry_interval = Интервал времени в секундах между попытками. По умолчанию: 600
|
||||||
|
***
|
||||||
|
#### English
|
||||||
|
* Added the ability to retry sending a notification if an error occurred the previous time.
|
||||||
|
* Added the `kor-elf-shield notifications queue count` command, which returns the number of notifications in the queue in the database.
|
||||||
|
* Added the `kor-elf-shield notifications queue clear` command, which removes all notifications from the queue in the database.
|
||||||
|
* New parameters have been added to the kor-elf-shield.toml settings file:
|
||||||
|
* data_dir = Directory for persistent application data (state): local database, cache/indexes, state files, and other internal data. Must be writable by the daemon user. If the directory does not exist, it will be created. Default: "/var/lib/kor-elf-shield/"
|
||||||
|
* New parameters have been added to the notifications.toml settings file:
|
||||||
|
* enable_retries = Enables repeated attempts to send a notification if the first attempt fails. Default: true
|
||||||
|
* retry_interval = The time interval in seconds between attempts. Default: 600
|
||||||
|
***
|
||||||
## 0.5.0 (17.1.2026)
|
## 0.5.0 (17.1.2026)
|
||||||
***
|
***
|
||||||
#### Русский
|
#### Русский
|
||||||
|
|||||||
@@ -710,6 +710,31 @@ SOFTWARE.
|
|||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
go.etcd.io/bbolt
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Ben Johnson
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
go.uber.org/multierr
|
go.uber.org/multierr
|
||||||
|
|
||||||
Copyright (c) 2017-2021 Uber Technologies, Inc.
|
Copyright (c) 2017-2021 Uber Technologies, Inc.
|
||||||
@@ -773,13 +798,13 @@ starting in 2011 when the project was ported over:
|
|||||||
- internal/libyaml/yaml.go
|
- internal/libyaml/yaml.go
|
||||||
- internal/libyaml/yamlprivate.go
|
- internal/libyaml/yamlprivate.go
|
||||||
|
|
||||||
Copyright 2006-2011 - Kirill Simonov
|
Copyright 2006-2010 Kirill Simonov
|
||||||
https://opensource.org/license/mit
|
https://opensource.org/license/mit
|
||||||
|
|
||||||
All the remaining project files are covered by the Apache license:
|
All the remaining project files are covered by the Apache license:
|
||||||
|
|
||||||
Copyright 2011-2019 - Canonical Ltd
|
Copyright 2011-2019 Canonical Ltd
|
||||||
Copyright 2025 - The go-yaml Project Contributors
|
Copyright 2025 The go-yaml Project Contributors
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -22,13 +22,13 @@
|
|||||||
testing = true
|
testing = true
|
||||||
|
|
||||||
###
|
###
|
||||||
# Тестовый период, по истечении которого брандмауэр удалит правила и демон завершит работу.
|
# Тестовый период, по истечении которого брандмауэр удалит правила, очистит другие данные и демон завершит работу.
|
||||||
# Период указывается в минутах.
|
# Период указывается в минутах.
|
||||||
# Мин: 1
|
# Мин: 1
|
||||||
# Макс: 30000
|
# Макс: 30000
|
||||||
# По умолчанию: 5
|
# По умолчанию: 5
|
||||||
# ***
|
# ***
|
||||||
# The test period after which the firewall will clear the rules and the daemon will shut down.
|
# A test period after which the firewall will remove rules, clear other data, and the daemon will exit.
|
||||||
# The period is specified in minutes.
|
# The period is specified in minutes.
|
||||||
# Min: 1
|
# Min: 1
|
||||||
# Max: 30000
|
# Max: 30000
|
||||||
@@ -76,6 +76,18 @@ pid_file = "/var/run/kor-elf-shield/kor-elf-shield.pid"
|
|||||||
###
|
###
|
||||||
socket_file = "/var/run/kor-elf-shield/kor-elf-shield.sock"
|
socket_file = "/var/run/kor-elf-shield/kor-elf-shield.sock"
|
||||||
|
|
||||||
|
###
|
||||||
|
# Каталог для постоянных данных приложения (state): локальная база данных, кэш/индексы, файлы состояния
|
||||||
|
# и другие служебные файлы. Должен быть доступен на запись пользователю, от имени которого запущен демон.
|
||||||
|
# Если каталог не существует — будет создан.
|
||||||
|
# По умолчанию: "/var/lib/kor-elf-shield/"
|
||||||
|
# ***
|
||||||
|
# Directory for persistent application data (state): local database, cache/indexes, state files, and other
|
||||||
|
# internal data. Must be writable by the daemon user. If the directory does not exist, it will be created.
|
||||||
|
# Default: "/var/lib/kor-elf-shield/"
|
||||||
|
###
|
||||||
|
data_dir = "/var/lib/kor-elf-shield/"
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# РАЗДЕЛ:Log
|
# РАЗДЕЛ:Log
|
||||||
# ***
|
# ***
|
||||||
|
|||||||
@@ -21,6 +21,35 @@
|
|||||||
###
|
###
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
|
###
|
||||||
|
# Включает повторные попытки отправить уведомление, если сразу не получилось.
|
||||||
|
# false = Выключает.
|
||||||
|
# true = Включает.
|
||||||
|
#
|
||||||
|
# По умолчанию: true
|
||||||
|
# ***
|
||||||
|
# Enables repeated attempts to send a notification if the first attempt fails.
|
||||||
|
# false = Disables.
|
||||||
|
# true = Enables.
|
||||||
|
#
|
||||||
|
# Default: true
|
||||||
|
###
|
||||||
|
enable_retries = true
|
||||||
|
|
||||||
|
###
|
||||||
|
# Интервал времени в секундах между попытками.
|
||||||
|
#
|
||||||
|
# По умолчанию: 600
|
||||||
|
# ***
|
||||||
|
# The time interval in seconds between attempts.
|
||||||
|
#
|
||||||
|
# Default: 600
|
||||||
|
###
|
||||||
|
retry_interval = 600
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Название сервера в уведомлениях
|
# Название сервера в уведомлениях
|
||||||
# По умолчанию: server
|
# По умолчанию: server
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/urfave/cli/v3 v3.4.1
|
github.com/urfave/cli/v3 v3.4.1
|
||||||
github.com/wneessen/go-mail v0.7.2
|
github.com/wneessen/go-mail v0.7.2
|
||||||
|
go.etcd.io/bbolt v1.4.3
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/sys v0.36.0
|
golang.org/x/sys v0.36.0
|
||||||
golang.org/x/text v0.29.0
|
golang.org/x/text v0.29.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -42,6 +42,8 @@ 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.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
|
||||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||||
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
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/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 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
@@ -50,6 +52,8 @@ 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.0/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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
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.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
|
|||||||
89
internal/cmd/daemon/notifications.go
Normal file
89
internal/cmd/daemon/notifications.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CmdNotifications() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "notifications",
|
||||||
|
Usage: i18n.Lang.T("cmd.daemon.notifications.Usage"),
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "queue",
|
||||||
|
Usage: i18n.Lang.T("cmd.daemon.notifications.queue.Usage"),
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "count",
|
||||||
|
Usage: i18n.Lang.T("cmd.daemon.notifications.queue.count.Usage"),
|
||||||
|
Description: i18n.Lang.T("cmd.daemon.notifications.queue.count.Description"),
|
||||||
|
Action: cmdNotificationsQueueCount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "clear",
|
||||||
|
Usage: i18n.Lang.T("cmd.daemon.notifications.queue.clear.Usage"),
|
||||||
|
Description: i18n.Lang.T("cmd.daemon.notifications.queue.clear.Description"),
|
||||||
|
Action: cmdNotificationsQueueClear,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = sock.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
result, err := sock.Send("notifications_queue_count")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(i18n.Lang.T("cmd.daemon.notifications.queue.count.result", map[string]interface{}{
|
||||||
|
"Count": result,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(i18n.Lang.T("daemon is not running"))
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = sock.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
result, err := sock.Send("notifications_queue_clear")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != "ok" {
|
||||||
|
return errors.New(i18n.Lang.T("notifications_queue_clear_error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(i18n.Lang.T("notifications_queue_clear_success"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon"
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon"
|
||||||
|
"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/db/repository"
|
||||||
"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/docker_monitor"
|
||||||
"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/notifications"
|
||||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||||
@@ -51,7 +53,19 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationsService, err := newNotificationsService(logger)
|
repositories, err := db.New(config.DataDir)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err.Error())
|
||||||
|
|
||||||
|
// Fatal should call os.Exit(1), but there's a chance that might not happen,
|
||||||
|
// so we return err just in case.return err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = repositories.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
notificationsService, err := newNotificationsService(repositories.NotificationsQueue(), logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err.Error())
|
logger.Fatal(err.Error())
|
||||||
|
|
||||||
@@ -81,13 +95,13 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNotificationsService(logger log.Logger) (notifications.Notifications, error) {
|
func newNotificationsService(queueRepository repository.NotificationsQueueRepository, logger log.Logger) (notifications.Notifications, error) {
|
||||||
config, err := setting.Config.OtherSettingsPath.ToNotificationsConfig()
|
config, err := setting.Config.OtherSettingsPath.ToNotificationsConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return notifications.New(config, logger), nil
|
return notifications.New(config, queueRepository, logger), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDockerService(ctx context.Context, logger log.Logger) (dockerService docker_monitor.Docker, dockerSupport bool, err error) {
|
func newDockerService(ctx context.Context, logger log.Logger) (dockerService docker_monitor.Docker, dockerSupport bool, err error) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func NewMainApp(appVer AppVersion, defaultConfigPath string) *cli.Command {
|
|||||||
daemon.CmdStop(),
|
daemon.CmdStop(),
|
||||||
daemon.CmdStatus(),
|
daemon.CmdStatus(),
|
||||||
daemon.CmdReopenLogger(),
|
daemon.CmdReopenLogger(),
|
||||||
|
daemon.CmdNotifications(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -27,28 +27,27 @@ type analyzer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
|
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||||
var units []string
|
var matches []string
|
||||||
|
alertRuleIndex := analysisServices.NewAlertRuleIndex()
|
||||||
|
|
||||||
if config.Login.Enabled {
|
for _, source := range config.Sources {
|
||||||
if config.Login.SSH.Enabled {
|
switch source.Type {
|
||||||
units = append(units, "_SYSTEMD_UNIT=ssh.service")
|
case config2.SourceTypeJournal:
|
||||||
|
match := source.Journal.JournalctlMatch()
|
||||||
|
matches = append(matches, match)
|
||||||
|
default:
|
||||||
|
logger.Error(fmt.Sprintf("Unknown source type: %s", source.Type))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Login.Local.Enabled {
|
err := alertRuleIndex.Add(source)
|
||||||
units = append(units, "SYSLOG_IDENTIFIER=login")
|
if err != nil {
|
||||||
}
|
logger.Error(fmt.Sprintf("Failed to add alert rule: %s", err))
|
||||||
|
|
||||||
if config.Login.Su.Enabled {
|
|
||||||
units = append(units, "SYSLOG_IDENTIFIER=su")
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Login.Sudo.Enabled {
|
|
||||||
units = append(units, "SYSLOG_IDENTIFIER=sudo")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, units, logger)
|
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, matches, logger)
|
||||||
analysisService := analyzerLog.NewAnalysis(&config, logger, notify)
|
analysisService := analyzerLog.NewAnalysis(alertRuleIndex, logger, notify)
|
||||||
|
|
||||||
return &analyzer{
|
return &analyzer{
|
||||||
config: config,
|
config: config,
|
||||||
@@ -77,28 +76,9 @@ func (a *analyzer) processLogs(ctx context.Context) {
|
|||||||
// Channel closed
|
// Channel closed
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
|
a.logger.Debug(fmt.Sprintf("Received log entry: %v", entry))
|
||||||
|
|
||||||
switch {
|
a.analysis.Alert(&entry)
|
||||||
case entry.Unit == "ssh.service":
|
|
||||||
if err := a.analysis.SSH(&entry); err != nil {
|
|
||||||
a.logger.Error(fmt.Sprintf("Failed to analyze SSH logs: %s", err))
|
|
||||||
}
|
|
||||||
case entry.SyslogIdentifier == "login":
|
|
||||||
if err := a.analysis.Locale(&entry); err != nil {
|
|
||||||
a.logger.Error(fmt.Sprintf("Failed to analyze locale logs: %s", err))
|
|
||||||
}
|
|
||||||
case entry.SyslogIdentifier == "sudo":
|
|
||||||
if err := a.analysis.Sudo(&entry); err != nil {
|
|
||||||
a.logger.Error(fmt.Sprintf("Failed to analyze sudo logs: %s", err))
|
|
||||||
}
|
|
||||||
case entry.SyslogIdentifier == "su":
|
|
||||||
if err := a.analysis.Su(&entry); err != nil {
|
|
||||||
a.logger.Error(fmt.Sprintf("Failed to analyze su logs: %s", err))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
a.logger.Debug(fmt.Sprintf("Unknown unit or SyslogIdentifier: %s", entry.Unit))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,90 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SourceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SourceTypeJournal SourceType = "journalctl"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JournalField string
|
||||||
|
|
||||||
|
const (
|
||||||
|
JournalFieldSystemdUnit JournalField = "_SYSTEMD_UNIT"
|
||||||
|
JournalFieldSyslogIdentifier JournalField = "SYSLOG_IDENTIFIER"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
BinPath BinPath
|
BinPath BinPath
|
||||||
Login Login
|
Sources []*Source
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceJournal struct {
|
||||||
|
Field JournalField
|
||||||
|
Match string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SourceJournal) JournalctlMatch() string {
|
||||||
|
return string(s.Field) + "=" + s.Match
|
||||||
|
}
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
Type SourceType
|
||||||
|
|
||||||
|
Journal *SourceJournal
|
||||||
|
AlertRule *AlertRule
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertRule struct {
|
||||||
|
Name string
|
||||||
|
Message string
|
||||||
|
IsNotification bool
|
||||||
|
Patterns []AlertRegexPattern
|
||||||
|
Group *AlertGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertRegexPattern struct {
|
||||||
|
Regexp *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,30 +1,148 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Login struct {
|
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||||
Enabled bool
|
|
||||||
Notify bool
|
func NewLoginSSH(isNotify bool) []*Source {
|
||||||
SSH LoginSSH
|
var sources []*Source
|
||||||
Local LoginLocal
|
|
||||||
Su LoginSu
|
source := &Source{
|
||||||
Sudo LoginSudo
|
Type: SourceTypeJournal,
|
||||||
|
Journal: &SourceJournal{
|
||||||
|
Field: JournalFieldSystemdUnit,
|
||||||
|
Match: "ssh.service",
|
||||||
|
},
|
||||||
|
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+`),
|
||||||
|
Values: []PatternValue{
|
||||||
|
{
|
||||||
|
Name: i18n.Lang.T("user"),
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "IP",
|
||||||
|
Value: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Group: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sources = append(sources, source)
|
||||||
|
|
||||||
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginSSH struct {
|
func NewLoginLocal(isNotify bool) []*Source {
|
||||||
Enabled bool
|
var sources []*Source
|
||||||
Notify bool
|
|
||||||
|
source := &Source{
|
||||||
|
Type: SourceTypeJournal,
|
||||||
|
Journal: &SourceJournal{
|
||||||
|
Field: JournalFieldSyslogIdentifier,
|
||||||
|
Match: "login",
|
||||||
|
},
|
||||||
|
|
||||||
|
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+`),
|
||||||
|
Values: []PatternValue{
|
||||||
|
{
|
||||||
|
Name: i18n.Lang.T("user"),
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Group: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sources = append(sources, source)
|
||||||
|
|
||||||
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginLocal struct {
|
func NewLoginSu(isNotify bool) []*Source {
|
||||||
Enabled bool
|
var sources []*Source
|
||||||
Notify bool
|
|
||||||
|
source := &Source{
|
||||||
|
Type: SourceTypeJournal,
|
||||||
|
Journal: &SourceJournal{
|
||||||
|
Field: JournalFieldSyslogIdentifier,
|
||||||
|
Match: "su",
|
||||||
|
},
|
||||||
|
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+\)`),
|
||||||
|
Values: []PatternValue{
|
||||||
|
{
|
||||||
|
Name: i18n.Lang.T("user"),
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: i18n.Lang.T("access to user has been gained"),
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Group: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sources = append(sources, source)
|
||||||
|
|
||||||
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginSu struct {
|
func NewLoginSudo(isNotify bool) []*Source {
|
||||||
Enabled bool
|
var sources []*Source
|
||||||
Notify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginSudo struct {
|
source := &Source{
|
||||||
Enabled bool
|
Type: SourceTypeJournal,
|
||||||
Notify bool
|
Journal: &SourceJournal{
|
||||||
|
Field: JournalFieldSyslogIdentifier,
|
||||||
|
Match: "sudo",
|
||||||
|
},
|
||||||
|
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+\)`),
|
||||||
|
Values: []PatternValue{
|
||||||
|
{
|
||||||
|
Name: i18n.Lang.T("user"),
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: i18n.Lang.T("access to user has been gained"),
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Group: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sources = append(sources, source)
|
||||||
|
|
||||||
|
return sources
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,25 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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"
|
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/notifications"
|
"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"
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Analysis interface {
|
type Analysis interface {
|
||||||
SSH(entry *analysisServices.Entry) error
|
Alert(entry *analysisServices.Entry)
|
||||||
Locale(entry *analysisServices.Entry) error
|
|
||||||
Su(entry *analysisServices.Entry) error
|
|
||||||
Sudo(entry *analysisServices.Entry) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type analysis struct {
|
type analysis struct {
|
||||||
sshService analysisServices.Analysis
|
alertService analysisServices.Alert
|
||||||
localeService analysisServices.Analysis
|
|
||||||
suService analysisServices.Analysis
|
|
||||||
sudoService analysisServices.Analysis
|
|
||||||
|
|
||||||
logger log.Logger
|
|
||||||
notify notifications.Notifications
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAnalysis(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
func NewAnalysis(alertRuleIndex analysisServices.AlertRuleIndex, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||||
return &analysis{
|
return &analysis{
|
||||||
sshService: analysisServices.NewSSH(config, logger, notify),
|
alertService: analysisServices.NewAlert(alertRuleIndex, logger, notify),
|
||||||
localeService: analysisServices.NewLocale(config, logger, notify),
|
|
||||||
suService: analysisServices.NewSu(config, logger, notify),
|
|
||||||
sudoService: analysisServices.NewSudo(config, logger, notify),
|
|
||||||
logger: logger,
|
|
||||||
notify: notify,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *analysis) SSH(entry *analysisServices.Entry) error {
|
func (a *analysis) Alert(entry *analysisServices.Entry) {
|
||||||
return a.sshService.Process(entry)
|
a.alertService.Analyze(entry)
|
||||||
}
|
|
||||||
|
|
||||||
func (a *analysis) Locale(entry *analysisServices.Entry) error {
|
|
||||||
return a.localeService.Process(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *analysis) Su(entry *analysisServices.Entry) error {
|
|
||||||
return a.suService.Process(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *analysis) Sudo(entry *analysisServices.Entry) error {
|
|
||||||
return a.sudoService.Process(entry)
|
|
||||||
}
|
}
|
||||||
|
|||||||
135
internal/daemon/analyzer/log/analysis/alert.go
Normal file
135
internal/daemon/analyzer/log/analysis/alert.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package analysis
|
||||||
|
|
||||||
|
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/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 Alert interface {
|
||||||
|
Analyze(entry *Entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
type alert struct {
|
||||||
|
ruleIndex AlertRuleIndex
|
||||||
|
logger log.Logger
|
||||||
|
notify notifications.Notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
type alertAnalyzeRuleReturn struct {
|
||||||
|
found bool
|
||||||
|
fields []*regexField
|
||||||
|
}
|
||||||
|
|
||||||
|
type alertNotify struct {
|
||||||
|
rule *config.AlertRule
|
||||||
|
messages []string
|
||||||
|
time time.Time
|
||||||
|
fields []*regexField
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAlert(ruleIndex AlertRuleIndex, logger log.Logger, notify notifications.Notifications) Alert {
|
||||||
|
return &alert{
|
||||||
|
ruleIndex: ruleIndex,
|
||||||
|
logger: logger,
|
||||||
|
notify: notify,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *alert) Analyze(entry *Entry) {
|
||||||
|
rules, err := a.ruleIndex.Rules(entry)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error(fmt.Sprintf("Failed to get alert rules: %s", err))
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
result := a.analyzeRule(rule, entry.Message)
|
||||||
|
if !result.found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groupName := ""
|
||||||
|
messages := []string{}
|
||||||
|
if rule.Group != nil {
|
||||||
|
groupName = rule.Group.Name
|
||||||
|
} else {
|
||||||
|
messages = append(messages, entry.Message)
|
||||||
|
}
|
||||||
|
a.logger.Info(fmt.Sprintf("Alert detected (%s) (group:%s): %s", rule.Name, groupName, entry.Message))
|
||||||
|
a.sendNotify(&alertNotify{
|
||||||
|
rule: rule,
|
||||||
|
messages: messages,
|
||||||
|
time: entry.Time,
|
||||||
|
fields: result.fields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *alert) analyzeRule(rule *config.AlertRule, message string) alertAnalyzeRuleReturn {
|
||||||
|
result := alertAnalyzeRuleReturn{
|
||||||
|
found: false,
|
||||||
|
fields: []*regexField{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range rule.Patterns {
|
||||||
|
re, err := pattern.Regexp.Get()
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error(fmt.Sprintf("Failed to compile regexp: %s", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := re.FindStringSubmatchIndex(message)
|
||||||
|
|
||||||
|
if idx != nil {
|
||||||
|
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: message[start:end]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pattern.Values) != len(result.fields) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.found = true
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *alert) sendNotify(notify *alertNotify) {
|
||||||
|
if !notify.rule.IsNotification {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
groupName := ""
|
||||||
|
groupMessage := ""
|
||||||
|
if notify.rule.Group != nil {
|
||||||
|
groupName = notify.rule.Group.Name
|
||||||
|
groupMessage = notify.rule.Group.Message + "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := i18n.Lang.T("alert.subject", map[string]any{
|
||||||
|
"Name": notify.rule.Name,
|
||||||
|
"GroupName": groupName,
|
||||||
|
})
|
||||||
|
text := subject + "\n\n" + groupMessage + notify.rule.Message + "\n\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"
|
||||||
|
}
|
||||||
|
a.notify.SendAsync(notifications.Message{Subject: subject, Body: text})
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package analysis
|
package analysis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Analysis interface {
|
|
||||||
Process(entry *Entry) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
|
Source config.SourceType
|
||||||
Message string
|
Message string
|
||||||
Unit string
|
Unit string
|
||||||
PID string
|
PID string
|
||||||
@@ -16,14 +16,22 @@ type Entry struct {
|
|||||||
Time time.Time
|
Time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type processReturn struct {
|
type regexField struct {
|
||||||
found bool
|
name string
|
||||||
subject string
|
value string
|
||||||
body string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmptyAnalysis struct{}
|
func getValueStartEndByRegexIndex(valueId int, idx []int) (start int, end int, err error) {
|
||||||
|
id := 2 * valueId
|
||||||
|
|
||||||
func (empty *EmptyAnalysis) Process(_ *Entry) error {
|
if idx == nil || len(idx) <= id+1 {
|
||||||
return nil
|
return 0, 0, errors.New("invalid index")
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end = idx[id], idx[id+1]
|
||||||
|
if start < 0 || end < 0 {
|
||||||
|
return 0, 0, errors.New("invalid index")
|
||||||
|
}
|
||||||
|
|
||||||
|
return start, end, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package analysis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"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/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 locale struct {
|
|
||||||
login localeLogin
|
|
||||||
|
|
||||||
logger log.Logger
|
|
||||||
notify notifications.Notifications
|
|
||||||
}
|
|
||||||
|
|
||||||
type localeLogin struct {
|
|
||||||
enabled bool
|
|
||||||
notify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLocale(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
|
||||||
if !config.Login.Enabled || !config.Login.Local.Enabled {
|
|
||||||
return &EmptyAnalysis{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &locale{
|
|
||||||
login: localeLogin{
|
|
||||||
enabled: config.Login.Enabled && config.Login.SSH.Enabled,
|
|
||||||
notify: config.Login.Notify && config.Login.SSH.Notify,
|
|
||||||
},
|
|
||||||
|
|
||||||
logger: logger,
|
|
||||||
notify: notify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *locale) Process(entry *Entry) error {
|
|
||||||
if l.login.enabled {
|
|
||||||
result, err := l.login.process(entry)
|
|
||||||
if err != nil {
|
|
||||||
l.logger.Error(fmt.Sprintf("Failed to process TTY login: %s", err))
|
|
||||||
} else if result.found {
|
|
||||||
if l.login.notify {
|
|
||||||
l.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
|
|
||||||
}
|
|
||||||
l.logger.Info(fmt.Sprintf("TTY login detected: %s", entry.Message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *localeLogin) process(entry *Entry) (processReturn, error) {
|
|
||||||
re := regexp.MustCompile(`^pam_unix\(login:session\): session opened for user (\S+)\(\S+\) by \S+`)
|
|
||||||
matches := re.FindStringSubmatch(entry.Message)
|
|
||||||
|
|
||||||
if matches != nil {
|
|
||||||
user := matches[1]
|
|
||||||
|
|
||||||
return processReturn{
|
|
||||||
found: true,
|
|
||||||
subject: i18n.Lang.T("alert.login.locale.subject", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
}),
|
|
||||||
body: i18n.Lang.T("alert.login.locale.body", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
"Log": entry.Message,
|
|
||||||
"Time": entry.Time,
|
|
||||||
}),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return processReturn{found: false}, nil
|
|
||||||
}
|
|
||||||
70
internal/daemon/analyzer/log/analysis/rule_index.go
Normal file
70
internal/daemon/analyzer/log/analysis/rule_index.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package analysis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"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/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 ssh struct {
|
|
||||||
login sshLogin
|
|
||||||
|
|
||||||
logger log.Logger
|
|
||||||
notify notifications.Notifications
|
|
||||||
}
|
|
||||||
|
|
||||||
type sshLogin struct {
|
|
||||||
enabled bool
|
|
||||||
notify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSSH(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
|
||||||
if !config.Login.Enabled || !config.Login.SSH.Enabled {
|
|
||||||
return &EmptyAnalysis{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ssh{
|
|
||||||
login: sshLogin{
|
|
||||||
enabled: config.Login.Enabled && config.Login.SSH.Enabled,
|
|
||||||
notify: config.Login.Notify && config.Login.SSH.Notify,
|
|
||||||
},
|
|
||||||
|
|
||||||
logger: logger,
|
|
||||||
notify: notify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ssh) Process(entry *Entry) error {
|
|
||||||
if s.login.enabled {
|
|
||||||
result, err := s.login.process(entry)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error(fmt.Sprintf("Failed to process ssh login: %s", err))
|
|
||||||
} else if result.found {
|
|
||||||
if s.login.notify {
|
|
||||||
s.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
|
|
||||||
}
|
|
||||||
s.logger.Info(fmt.Sprintf("SSH login detected: %s", entry.Message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *sshLogin) process(entry *Entry) (processReturn, error) {
|
|
||||||
re := regexp.MustCompile(`^Accepted (\S+) for (\S+) from (\S+) port \S+`)
|
|
||||||
matches := re.FindStringSubmatch(entry.Message)
|
|
||||||
|
|
||||||
if matches != nil {
|
|
||||||
user := matches[2]
|
|
||||||
ip := matches[3]
|
|
||||||
|
|
||||||
return processReturn{
|
|
||||||
found: true,
|
|
||||||
subject: i18n.Lang.T("alert.login.ssh.subject", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
"IP": ip,
|
|
||||||
}),
|
|
||||||
body: i18n.Lang.T("alert.login.ssh.body", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
"IP": ip,
|
|
||||||
"Log": entry.Message,
|
|
||||||
"Time": entry.Time,
|
|
||||||
}),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return processReturn{found: false}, nil
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package analysis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"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/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 su struct {
|
|
||||||
login suLogin
|
|
||||||
|
|
||||||
logger log.Logger
|
|
||||||
notify notifications.Notifications
|
|
||||||
}
|
|
||||||
|
|
||||||
type suLogin struct {
|
|
||||||
enabled bool
|
|
||||||
notify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSu(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
|
||||||
if !config.Login.Enabled || !config.Login.Su.Enabled {
|
|
||||||
return &EmptyAnalysis{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &su{
|
|
||||||
login: suLogin{
|
|
||||||
enabled: config.Login.Enabled && config.Login.Su.Enabled,
|
|
||||||
notify: config.Login.Notify && config.Login.Su.Notify,
|
|
||||||
},
|
|
||||||
|
|
||||||
logger: logger,
|
|
||||||
notify: notify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *su) Process(entry *Entry) error {
|
|
||||||
if l.login.enabled {
|
|
||||||
result, err := l.login.process(entry)
|
|
||||||
if err != nil {
|
|
||||||
l.logger.Error(fmt.Sprintf("Failed to process Su login: %s", err))
|
|
||||||
} else if result.found {
|
|
||||||
if l.login.notify {
|
|
||||||
l.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
|
|
||||||
}
|
|
||||||
l.logger.Info(fmt.Sprintf("Su login detected: %s", entry.Message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *suLogin) process(entry *Entry) (processReturn, error) {
|
|
||||||
re := regexp.MustCompile(`^pam_unix\(su:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`)
|
|
||||||
matches := re.FindStringSubmatch(entry.Message)
|
|
||||||
|
|
||||||
if matches != nil {
|
|
||||||
user := matches[1]
|
|
||||||
byUser := matches[2]
|
|
||||||
|
|
||||||
return processReturn{
|
|
||||||
found: true,
|
|
||||||
subject: i18n.Lang.T("alert.login.su.subject", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
"ByUser": byUser,
|
|
||||||
}),
|
|
||||||
body: i18n.Lang.T("alert.login.su.body", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
"ByUser": byUser,
|
|
||||||
"Log": entry.Message,
|
|
||||||
"Time": entry.Time,
|
|
||||||
}),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return processReturn{found: false}, nil
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package analysis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"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/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 sudo struct {
|
|
||||||
login sudoLogin
|
|
||||||
|
|
||||||
logger log.Logger
|
|
||||||
notify notifications.Notifications
|
|
||||||
}
|
|
||||||
|
|
||||||
type sudoLogin struct {
|
|
||||||
enabled bool
|
|
||||||
notify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSudo(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
|
||||||
if !config.Login.Enabled || !config.Login.Su.Enabled {
|
|
||||||
return &EmptyAnalysis{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &sudo{
|
|
||||||
login: sudoLogin{
|
|
||||||
enabled: config.Login.Enabled && config.Login.Sudo.Enabled,
|
|
||||||
notify: config.Login.Notify && config.Login.Sudo.Notify,
|
|
||||||
},
|
|
||||||
|
|
||||||
logger: logger,
|
|
||||||
notify: notify,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sudo) Process(entry *Entry) error {
|
|
||||||
if s.login.enabled {
|
|
||||||
result, err := s.login.process(entry)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error(fmt.Sprintf("Failed to process Sudo login: %s", err))
|
|
||||||
} else if result.found {
|
|
||||||
if s.login.notify {
|
|
||||||
s.notify.SendAsync(notifications.Message{Subject: result.subject, Body: result.body})
|
|
||||||
}
|
|
||||||
s.logger.Info(fmt.Sprintf("Sudo login detected: %s", entry.Message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sudoLogin) process(entry *Entry) (processReturn, error) {
|
|
||||||
re := regexp.MustCompile(`^pam_unix\(sudo:session\): session opened for user (\S+)\(\S+\) by (\S+)\(\S+\)`)
|
|
||||||
matches := re.FindStringSubmatch(entry.Message)
|
|
||||||
|
|
||||||
if matches != nil {
|
|
||||||
user := matches[1]
|
|
||||||
byUser := matches[2]
|
|
||||||
|
|
||||||
return processReturn{
|
|
||||||
found: true,
|
|
||||||
subject: i18n.Lang.T("alert.login.sudo.subject", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
"ByUser": byUser,
|
|
||||||
}),
|
|
||||||
body: i18n.Lang.T("alert.login.sudo.body", map[string]any{
|
|
||||||
"User": user,
|
|
||||||
"ByUser": byUser,
|
|
||||||
"Log": entry.Message,
|
|
||||||
"Time": entry.Time,
|
|
||||||
}),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return processReturn{found: false}, nil
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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"
|
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/log"
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||||
)
|
)
|
||||||
@@ -20,9 +21,9 @@ type Systemd interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type systemd struct {
|
type systemd struct {
|
||||||
path string
|
path string
|
||||||
units []string
|
matches []string
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
|
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -37,17 +38,17 @@ type journalRawEntry struct {
|
|||||||
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
|
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSystemd(path string, units []string, logger log.Logger) Systemd {
|
func NewSystemd(path string, matches []string, logger log.Logger) Systemd {
|
||||||
return &systemd{
|
return &systemd{
|
||||||
path: path,
|
path: path,
|
||||||
units: units,
|
matches: matches,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemd) Run(ctx context.Context, logChan chan<- analysisServices.Entry) {
|
func (s *systemd) Run(ctx context.Context, logChan chan<- analysisServices.Entry) {
|
||||||
if len(s.units) == 0 {
|
if len(s.matches) == 0 {
|
||||||
s.logger.Debug("No units specified for journalctl")
|
s.logger.Debug("No matches specified for journalctl")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,11 +76,11 @@ func (s *systemd) Run(ctx context.Context, logChan chan<- analysisServices.Entry
|
|||||||
|
|
||||||
func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Entry) error {
|
func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Entry) error {
|
||||||
args := []string{"-f", "-n", "0", "-o", "json"}
|
args := []string{"-f", "-n", "0", "-o", "json"}
|
||||||
for index, unit := range s.units {
|
for index, match := range s.matches {
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
args = append(args, "+")
|
args = append(args, "+")
|
||||||
}
|
}
|
||||||
args = append(args, unit)
|
args = append(args, match)
|
||||||
}
|
}
|
||||||
cmd := exec.CommandContext(ctx, s.path, args...)
|
cmd := exec.CommandContext(ctx, s.path, args...)
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
logChan <- analysisServices.Entry{
|
logChan <- analysisServices.Entry{
|
||||||
|
Source: config.SourceTypeJournal,
|
||||||
Message: raw.Message,
|
Message: raw.Message,
|
||||||
Unit: raw.Unit,
|
Unit: raw.Unit,
|
||||||
PID: raw.PID,
|
PID: raw.PID,
|
||||||
@@ -131,7 +133,7 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemd) Close() error {
|
func (s *systemd) Close() error {
|
||||||
if s.units == nil {
|
if s.matches == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"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/analyzer"
|
||||||
@@ -104,6 +106,10 @@ func (d *daemon) runWorker(ctx context.Context, isTesting bool, testingInterval
|
|||||||
return
|
return
|
||||||
case <-stopTestingCh:
|
case <-stopTestingCh:
|
||||||
d.logger.Info("Testing interval expired, stopping service")
|
d.logger.Info("Testing interval expired, stopping service")
|
||||||
|
err := d.notifications.DBQueueClear()
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Error(fmt.Sprintf("failed to clear notifications queue: %v", err))
|
||||||
|
}
|
||||||
d.Stop()
|
d.Stop()
|
||||||
return
|
return
|
||||||
case <-d.stopCh:
|
case <-d.stopCh:
|
||||||
@@ -126,6 +132,15 @@ func (d *daemon) socketCommand(command string, socket socket.Connect) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return socket.Write("ok")
|
return socket.Write("ok")
|
||||||
|
case "notifications_queue_count":
|
||||||
|
count := d.notifications.DBQueueSize()
|
||||||
|
return socket.Write(strconv.Itoa(count))
|
||||||
|
case "notifications_queue_clear":
|
||||||
|
if err := d.notifications.DBQueueClear(); err != nil {
|
||||||
|
_ = socket.Write("notifications queue clear failed: " + err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return socket.Write("ok")
|
||||||
default:
|
default:
|
||||||
_ = socket.Write("unknown command")
|
_ = socket.Write("unknown command")
|
||||||
return errors.New("unknown command")
|
return errors.New("unknown command")
|
||||||
|
|||||||
60
internal/daemon/db/db.go
Normal file
60
internal/daemon/db/db.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/pkg/filesystem"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
app = "app.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repositories interface {
|
||||||
|
NotificationsQueue() repository.NotificationsQueueRepository
|
||||||
|
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type repositories struct {
|
||||||
|
notificationsQueue repository.NotificationsQueueRepository
|
||||||
|
|
||||||
|
db []*bbolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(dataDir string) (Repositories, error) {
|
||||||
|
if dataDir == "" {
|
||||||
|
return &repositories{}, errors.New("data directory is empty")
|
||||||
|
}
|
||||||
|
if dataDir[len(dataDir)-1:] != "/" {
|
||||||
|
dataDir += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := filesystem.EnsureDir(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
return &repositories{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
appDB, err := bbolt.Open(dataDir+app, 0600, &bbolt.Options{Timeout: 3 * time.Second})
|
||||||
|
|
||||||
|
return &repositories{
|
||||||
|
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
|
||||||
|
|
||||||
|
db: []*bbolt.DB{appDB},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repositories) NotificationsQueue() repository.NotificationsQueueRepository {
|
||||||
|
return r.notificationsQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repositories) Close() error {
|
||||||
|
for _, db := range r.db {
|
||||||
|
_ = db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
6
internal/daemon/db/entity/notifications_queue.go
Normal file
6
internal/daemon/db/entity/notifications_queue.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type NotificationsQueue struct {
|
||||||
|
Subject string `json:"Subject"`
|
||||||
|
Body string `json:"Body"`
|
||||||
|
}
|
||||||
121
internal/daemon/db/repository/notifications_queue.go
Normal file
121
internal/daemon/db/repository/notifications_queue.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"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 NotificationsQueueRepository interface {
|
||||||
|
Add(q entity.NotificationsQueue) error
|
||||||
|
Get(limit int) (map[string]entity.NotificationsQueue, error)
|
||||||
|
Delete(id string) error
|
||||||
|
|
||||||
|
// Count - return size of notifications queue in db
|
||||||
|
Count() (int, error)
|
||||||
|
Clear() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type notificationsQueueRepository struct {
|
||||||
|
db *bbolt.DB
|
||||||
|
bucket string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationsQueueRepository(appDB *bbolt.DB) NotificationsQueueRepository {
|
||||||
|
return ¬ificationsQueueRepository{
|
||||||
|
db: appDB,
|
||||||
|
bucket: notificationsQueue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationsQueueRepository) Add(q entity.NotificationsQueue) 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(q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := nextID(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Put(id, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationsQueueRepository) Get(limit int) (map[string]entity.NotificationsQueue, error) {
|
||||||
|
notifications := make(map[string]entity.NotificationsQueue)
|
||||||
|
|
||||||
|
if limit <= 0 {
|
||||||
|
return notifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(r.bucket))
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := bucket.Cursor()
|
||||||
|
for k, v := c.First(); k != nil && len(notifications) < limit; k, v = c.Next() {
|
||||||
|
var q entity.NotificationsQueue
|
||||||
|
if err := json.Unmarshal(v, &q); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
notifications[string(k)] = q
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return notifications, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationsQueueRepository) Delete(id string) error {
|
||||||
|
return r.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(r.bucket))
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Delete([]byte(id))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationsQueueRepository) Count() (int, error) {
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
err := r.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(r.bucket))
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
count = bucket.Stats().KeyN
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notificationsQueueRepository) 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
|
||||||
|
})
|
||||||
|
}
|
||||||
22
internal/daemon/db/repository/repository.go
Normal file
22
internal/daemon/db/repository/repository.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
notificationsQueue = "notifications_queue"
|
||||||
|
)
|
||||||
|
|
||||||
|
func nextID(b *bbolt.Bucket) ([]byte, error) {
|
||||||
|
seq, err := b.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(key, seq)
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
@@ -5,9 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
ServerName string
|
EnableRetries bool
|
||||||
Email Email
|
RetryInterval uint16
|
||||||
|
ServerName string
|
||||||
|
Email Email
|
||||||
}
|
}
|
||||||
|
|
||||||
type Email struct {
|
type Email struct {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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/log"
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/log"
|
||||||
"github.com/wneessen/go-mail"
|
"github.com/wneessen/go-mail"
|
||||||
)
|
)
|
||||||
@@ -19,21 +21,26 @@ type Message struct {
|
|||||||
type Notifications interface {
|
type Notifications interface {
|
||||||
Run()
|
Run()
|
||||||
SendAsync(message Message)
|
SendAsync(message Message)
|
||||||
|
// DBQueueSize - return size of notifications queue in db
|
||||||
|
DBQueueSize() int
|
||||||
|
DBQueueClear() error
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type notifications struct {
|
type notifications struct {
|
||||||
config Config
|
config Config
|
||||||
logger log.Logger
|
queueRepository repository.NotificationsQueueRepository
|
||||||
msgQueue chan Message
|
logger log.Logger
|
||||||
wg sync.WaitGroup
|
msgQueue chan Message
|
||||||
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config Config, logger log.Logger) Notifications {
|
func New(config Config, queueRepository repository.NotificationsQueueRepository, logger log.Logger) Notifications {
|
||||||
return ¬ifications{
|
return ¬ifications{
|
||||||
config: config,
|
config: config,
|
||||||
logger: logger,
|
queueRepository: queueRepository,
|
||||||
msgQueue: make(chan Message, 100),
|
logger: logger,
|
||||||
|
msgQueue: make(chan Message, 100),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +52,46 @@ func (n *notifications) Run() {
|
|||||||
n.wg.Add(1)
|
n.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer n.wg.Done()
|
defer n.wg.Done()
|
||||||
for msg := range n.msgQueue {
|
|
||||||
err := n.sendEmail(msg)
|
ticker := time.NewTicker(time.Duration(n.config.RetryInterval) * time.Second)
|
||||||
if err != nil {
|
defer ticker.Stop()
|
||||||
n.logger.Error(fmt.Sprintf("failed to send email: %v", err))
|
|
||||||
} else if n.config.Enabled {
|
for {
|
||||||
n.logger.Debug(fmt.Sprintf("email sent: Subject %s, Body %s", msg.Subject, msg.Body))
|
select {
|
||||||
|
case msg, ok := <-n.msgQueue:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := n.sendEmail(msg)
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error(fmt.Sprintf("failed to send email: %v", err))
|
||||||
|
n.addNotificationsQueue(msg)
|
||||||
|
} else if n.config.Enabled {
|
||||||
|
n.logger.Debug(fmt.Sprintf("email sent: Subject %s, Body %s", msg.Subject, msg.Body))
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
if n.config.Enabled == false || n.config.EnableRetries == false {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := n.queueRepository.Get(10)
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error(fmt.Sprintf("failed to get notifications from the queue: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, item := range items {
|
||||||
|
err = n.sendEmail(Message{Subject: item.Subject, Body: item.Body})
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error(fmt.Sprintf("failed to send queued email: %v", err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.queueRepository.Delete(id)
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error(fmt.Sprintf("failed to delete queued email from the queue: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -66,9 +107,27 @@ func (n *notifications) SendAsync(message Message) {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
n.logger.Error(fmt.Sprintf("failed to send email: queue is full"))
|
n.logger.Error(fmt.Sprintf("failed to send email: queue is full"))
|
||||||
|
n.addNotificationsQueue(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *notifications) DBQueueSize() int {
|
||||||
|
count, err := n.queueRepository.Count()
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error(fmt.Sprintf("failed to get notifications queue size: %v", err))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *notifications) DBQueueClear() error {
|
||||||
|
err := n.queueRepository.Clear()
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error(fmt.Sprintf("failed to clear notifications queue: %v", err))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (n *notifications) Close() error {
|
func (n *notifications) Close() error {
|
||||||
close(n.msgQueue)
|
close(n.msgQueue)
|
||||||
n.logger.Debug("We are waiting for all notifications to be sent")
|
n.logger.Debug("We are waiting for all notifications to be sent")
|
||||||
@@ -104,6 +163,17 @@ func (n *notifications) sendEmail(message Message) error {
|
|||||||
return client.DialAndSendWithContext(ctx, m)
|
return client.DialAndSendWithContext(ctx, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *notifications) addNotificationsQueue(message Message) {
|
||||||
|
if n.config.Enabled == false || n.config.EnableRetries == false {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := n.queueRepository.Add(entity.NotificationsQueue{Body: message.Body, Subject: message.Subject})
|
||||||
|
if err != nil {
|
||||||
|
n.logger.Error(fmt.Sprintf("failed to save email to the queue: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newClient(config Email) (*mail.Client, error) {
|
func newClient(config Email) (*mail.Client, error) {
|
||||||
options := []mail.Option{
|
options := []mail.Option{
|
||||||
mail.WithPort(int(config.Port)),
|
mail.WithPort(int(config.Port)),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
type DaemonOptions struct {
|
type DaemonOptions struct {
|
||||||
PathPidFile string
|
PathPidFile string
|
||||||
PathSocketFile string
|
PathSocketFile string
|
||||||
|
DataDir string
|
||||||
PathNftables string
|
PathNftables string
|
||||||
ConfigFirewall firewall.Config
|
ConfigFirewall firewall.Config
|
||||||
ConfigAnalyzer config.Config
|
ConfigAnalyzer config.Config
|
||||||
|
|||||||
@@ -15,6 +15,16 @@
|
|||||||
"cmd.daemon.reopen_logger.Usage": "Reopen the file for logging",
|
"cmd.daemon.reopen_logger.Usage": "Reopen the file for logging",
|
||||||
"cmd.daemon.reopen_logger.Description": "Reopen the file where the daemon's logs are written",
|
"cmd.daemon.reopen_logger.Description": "Reopen the file where the daemon's logs are written",
|
||||||
|
|
||||||
|
"cmd.daemon.notifications.Usage": "Notifications",
|
||||||
|
"cmd.daemon.notifications.queue.Usage": "Notification queue",
|
||||||
|
"cmd.daemon.notifications.queue.count.Usage": "Number of notifications in the pending queue",
|
||||||
|
"cmd.daemon.notifications.queue.count.Description": "The number of notifications waiting to be sent after an error.",
|
||||||
|
"cmd.daemon.notifications.queue.count.result": "Number in backlog queue: {{.Count}}",
|
||||||
|
"cmd.daemon.notifications.queue.clear.Usage": "Clear the notification queue",
|
||||||
|
"cmd.daemon.notifications.queue.clear.Description": "Clear the queue of notifications waiting to be sent after an error.",
|
||||||
|
"notifications_queue_clear_error": "Failed to clear notification queue",
|
||||||
|
"notifications_queue_clear_success": "The notification queue has been cleared.",
|
||||||
|
|
||||||
"Command error": "Command error",
|
"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 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}}",
|
"invalid log encoding": "Invalid encoding setting. Currently set to: {{.Encoding}}. Valid values: {{.Encodings}}",
|
||||||
@@ -27,15 +37,14 @@
|
|||||||
"daemon is not running": "Daemon is not running",
|
"daemon is not running": "Daemon is not running",
|
||||||
"daemon is not reopening logger": "The daemon did not reopen the log",
|
"daemon is not reopening logger": "The daemon did not reopen the log",
|
||||||
|
|
||||||
"alert.login.ssh.subject": "SSH login alert for user {{.User}} from {{.IP}}",
|
"time": "Time: {{.Time}}",
|
||||||
"alert.login.ssh.body": "Logged into the OS via ssh:\n Time: {{.Time}}\n IP: {{.IP}}\n User: {{.User}}\n Log: {{.Log}}",
|
"log": "Log: ",
|
||||||
|
"user": "User",
|
||||||
|
"access to user has been gained": "Access to user has been gained",
|
||||||
|
|
||||||
"alert.login.locale.subject": "Login message for user {{.User}} (TTY)",
|
"alert.subject": "Alert detected ({{.Name}}) (group:{{.GroupName}})",
|
||||||
"alert.login.locale.body": "Logged into the OS via TTY:\n Time: {{.Time}}\n User: {{.User}}\n Log: {{.Log}}",
|
"alert.login.ssh.message": "Logged into the OS via ssh.",
|
||||||
|
"alert.login.local.message": "Logged into the OS via TTY.",
|
||||||
"alert.login.su.subject": "User {{.ByUser}} has accessed user {{.User}} via su",
|
"alert.login.su.message": "Gained access to another user via su.",
|
||||||
"alert.login.su.body": "User {{.ByUser}} accessed user {{.User}} via su.\nTime: {{.Time}}\nLog: {{.Log}}",
|
"alert.login.sudo.message": "Gained access to another user via sudo."
|
||||||
|
|
||||||
"alert.login.sudo.subject": "User {{.ByUser}} has accessed user {{.User}} via sudo",
|
|
||||||
"alert.login.sudo.body": "User {{.ByUser}} accessed user {{.User}} via sudo.\nTime: {{.Time}}\nLog: {{.Log}}"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,16 @@
|
|||||||
"cmd.daemon.reopen_logger.Usage": "Файлды тіркеу үшін қайта ашыңыз",
|
"cmd.daemon.reopen_logger.Usage": "Файлды тіркеу үшін қайта ашыңыз",
|
||||||
"cmd.daemon.reopen_logger.Description": "Демонның журналдары жазылған файлды қайта ашыңыз.",
|
"cmd.daemon.reopen_logger.Description": "Демонның журналдары жазылған файлды қайта ашыңыз.",
|
||||||
|
|
||||||
|
"cmd.daemon.notifications.Usage": "Хабарландырулар",
|
||||||
|
"cmd.daemon.notifications.queue.Usage": "Хабарландыру кезегі",
|
||||||
|
"cmd.daemon.notifications.queue.count.Usage": "Күтудегі кезектегі хабарландырулар саны",
|
||||||
|
"cmd.daemon.notifications.queue.count.Description": "Қатеден кейін жіберуді күтіп тұрған хабарландырулар саны.",
|
||||||
|
"cmd.daemon.notifications.queue.count.result": "Кезекте тұрған нөмір: {{.Count}}",
|
||||||
|
"cmd.daemon.notifications.queue.clear.Usage": "Хабарландыру кезегін тазалау",
|
||||||
|
"cmd.daemon.notifications.queue.clear.Description": "Қатеден кейін жіберуді күтіп тұрған хабарландырулар кезегін тазалаңыз.",
|
||||||
|
"notifications_queue_clear_error": "Хабарландыру кезегі тазаланбады",
|
||||||
|
"notifications_queue_clear_success": "Хабарландыру кезегі тазартылды",
|
||||||
|
|
||||||
"Command error": "Командалық қате",
|
"Command error": "Командалық қате",
|
||||||
"invalid log level": "Параметрлерде көрсетілген журнал деңгейі жарамсыз. Ол қазір мына күйге орнатылған: {{.Level}}. Жарамды мәндер: {{.Levels}}",
|
"invalid log level": "Параметрлерде көрсетілген журнал деңгейі жарамсыз. Ол қазір мына күйге орнатылған: {{.Level}}. Жарамды мәндер: {{.Levels}}",
|
||||||
"invalid log encoding": "Жарамсыз кодтау параметрі. Қазіргі уақытта орнатылған: {{.Encoding}}. Жарамды мәндер: {{.Encodings}}",
|
"invalid log encoding": "Жарамсыз кодтау параметрі. Қазіргі уақытта орнатылған: {{.Encoding}}. Жарамды мәндер: {{.Encodings}}",
|
||||||
@@ -27,15 +37,14 @@
|
|||||||
"daemon is not running": "Демон жұмыс істемейді",
|
"daemon is not running": "Демон жұмыс істемейді",
|
||||||
"daemon is not reopening logger": "Жын журналды қайта ашпады",
|
"daemon is not reopening logger": "Жын журналды қайта ашпады",
|
||||||
|
|
||||||
"alert.login.ssh.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
|
"time": "Уақыт: {{.Time}}",
|
||||||
"alert.login.ssh.body": "ОС-қа ssh арқылы кірді:\n Уақыт: {{.Time}}\n IP: {{.IP}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
|
"log": "Лог: ",
|
||||||
|
"user": "Пайдаланушы",
|
||||||
|
"access to user has been gained": "Пайдаланушыға кіру мүмкіндігі алынды",
|
||||||
|
|
||||||
"alert.login.locale.subject": "{{.User}} пайдаланушысына арналған кіру хабарламасы (TTY)",
|
"alert.subject": "Ескерту анықталды ({{.Name}}) (топ:{{.GroupName}})",
|
||||||
"alert.login.locale.body": "ОЖ-ға TTY арқылы кірдіңіз:\n Уақыт: {{.Time}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
|
"alert.login.ssh.message": "ОС-қа ssh арқылы кірді.",
|
||||||
|
"alert.login.local.message": "ОЖ-ға TTY арқылы кірдіңіз.",
|
||||||
"alert.login.su.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кіру мүмкіндігін алды",
|
"alert.login.su.message": "su арқылы басқа пайдаланушыға кіру мүмкіндігі алынды.",
|
||||||
"alert.login.su.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}",
|
"alert.login.sudo.message": "sudo арқылы басқа пайдаланушыға кіру мүмкіндігі алынды."
|
||||||
|
|
||||||
"alert.login.sudo.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кіру мүмкіндігін алды",
|
|
||||||
"alert.login.sudo.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}"
|
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,16 @@
|
|||||||
"cmd.daemon.reopen_logger.Usage": "Переоткрыть файл для логирования",
|
"cmd.daemon.reopen_logger.Usage": "Переоткрыть файл для логирования",
|
||||||
"cmd.daemon.reopen_logger.Description": "Переоткроет файл, куда пишутся логи от демона",
|
"cmd.daemon.reopen_logger.Description": "Переоткроет файл, куда пишутся логи от демона",
|
||||||
|
|
||||||
|
"cmd.daemon.notifications.Usage": "Уведомления",
|
||||||
|
"cmd.daemon.notifications.queue.Usage": "Очередь уведомлений",
|
||||||
|
"cmd.daemon.notifications.queue.count.Usage": "Количество уведомлений в отложенной очереди",
|
||||||
|
"cmd.daemon.notifications.queue.count.Description": "Количество уведомлений, ожидающих отправки после ошибки.",
|
||||||
|
"cmd.daemon.notifications.queue.count.result": "Количество в отложенной очереди: {{.Count}}",
|
||||||
|
"cmd.daemon.notifications.queue.clear.Usage": "Очистить очередь уведомлений",
|
||||||
|
"cmd.daemon.notifications.queue.clear.Description": "Очистить очередь уведомлений, ожидающих отправки после ошибки.",
|
||||||
|
"notifications_queue_clear_error": "Не удалось очистить очередь уведомлений",
|
||||||
|
"notifications_queue_clear_success": "Очередь уведомлений очищена",
|
||||||
|
|
||||||
"Command error": "Ошибка команды",
|
"Command error": "Ошибка команды",
|
||||||
"invalid log level": "В настройках указан не верный уровень log. Сейчас указан: {{.Level}}. Допустимые значения: {{.Levels}}",
|
"invalid log level": "В настройках указан не верный уровень log. Сейчас указан: {{.Level}}. Допустимые значения: {{.Levels}}",
|
||||||
"invalid log encoding": "Неверная настройка encoding. Сейчас указан: {{.Encoding}}. Допустимые значения: {{.Encodings}}",
|
"invalid log encoding": "Неверная настройка encoding. Сейчас указан: {{.Encoding}}. Допустимые значения: {{.Encodings}}",
|
||||||
@@ -27,15 +37,14 @@
|
|||||||
"daemon is not running": "Демон не запущен",
|
"daemon is not running": "Демон не запущен",
|
||||||
"daemon is not reopening logger": "Демон не открыл журнал повторно",
|
"daemon is not reopening logger": "Демон не открыл журнал повторно",
|
||||||
|
|
||||||
"alert.login.ssh.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
|
"time": "Время: {{.Time}}",
|
||||||
"alert.login.ssh.body": "Вошли в ОС через ssh:\n Время: {{.Time}}\n IP: {{.IP}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
|
"log": "Лог: ",
|
||||||
|
"user": "Пользователь",
|
||||||
|
"access to user has been gained": "Получен доступ к пользователю",
|
||||||
|
|
||||||
"alert.login.locale.subject": "Сообщение о входе пользователя {{.User}} (TTY)",
|
"alert.subject": "Обнаружено оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||||
"alert.login.locale.body": "Вошли в ОС через TTY:\n Время: {{.Time}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
|
"alert.login.ssh.message": "Вошли в ОС через ssh.",
|
||||||
|
"alert.login.local.message": "Вошли в ОС через TTY.",
|
||||||
"alert.login.su.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su",
|
"alert.login.su.message": "Получили доступ к другому пользователю через su.",
|
||||||
"alert.login.su.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su.\nВремя: {{.Time}}\nЛог: {{.Log}}",
|
"alert.login.sudo.message": "Получили доступ к другому пользователю через sudo."
|
||||||
|
|
||||||
"alert.login.sudo.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo",
|
|
||||||
"alert.login.sudo.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo.\nВремя: {{.Time}}\nЛог: {{.Log}}"
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package analyzer
|
package analyzer
|
||||||
|
|
||||||
import (
|
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/setting/validate"
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/setting/validate"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,18 @@ func settingDefault() Setting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Setting) ToSources() ([]*config.Source, error) {
|
||||||
|
var sources []*config.Source
|
||||||
|
|
||||||
|
loginSources, err := s.Login.ToSources()
|
||||||
|
if err != nil {
|
||||||
|
return sources, err
|
||||||
|
}
|
||||||
|
sources = append(sources, loginSources...)
|
||||||
|
|
||||||
|
return sources, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s Setting) Validate() error {
|
func (s Setting) Validate() error {
|
||||||
if err := s.Login.Validate(); err != nil {
|
if err := s.Login.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package analyzer
|
package analyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||||
|
)
|
||||||
|
|
||||||
type Login struct {
|
type Login struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `mapstructure:"enabled"`
|
||||||
Notify bool `mapstructure:"notify"`
|
Notify bool `mapstructure:"notify"`
|
||||||
@@ -39,3 +43,29 @@ func defaultLogin() Login {
|
|||||||
func (l Login) Validate() error {
|
func (l Login) Validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l Login) ToSources() ([]*config.Source, error) {
|
||||||
|
var sources []*config.Source
|
||||||
|
|
||||||
|
if !l.Enabled {
|
||||||
|
return sources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.SSHEnable {
|
||||||
|
sources = append(sources, config.NewLoginSSH(l.Notify && l.SSHNotify)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.LocalEnable {
|
||||||
|
sources = append(sources, config.NewLoginLocal(l.Notify && l.LocalNotify)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.SuEnable {
|
||||||
|
sources = append(sources, config.NewLoginSu(l.Notify && l.SuNotify)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.SudoEnable {
|
||||||
|
sources = append(sources, config.NewLoginSudo(l.Notify && l.SudoNotify)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `mapstructure:"enabled"`
|
||||||
ServerName string `mapstructure:"server_name"`
|
EnableRetries bool `mapstructure:"enable_retries"`
|
||||||
Email Email
|
RetryInterval int16 `mapstructure:"retry_interval"`
|
||||||
|
ServerName string `mapstructure:"server_name"`
|
||||||
|
Email Email
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitSetting(path string) (Setting, error) {
|
func InitSetting(path string) (Setting, error) {
|
||||||
@@ -44,9 +46,11 @@ func InitSetting(path string) (Setting, error) {
|
|||||||
|
|
||||||
func settingDefault() Setting {
|
func settingDefault() Setting {
|
||||||
return Setting{
|
return Setting{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
ServerName: "server",
|
EnableRetries: true,
|
||||||
Email: defaultEmail(),
|
RetryInterval: 600,
|
||||||
|
ServerName: "server",
|
||||||
|
Email: defaultEmail(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,5 +67,9 @@ func (s Setting) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.RetryInterval < 1 {
|
||||||
|
return errors.New("retry_interval must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,8 +115,10 @@ func (o *otherSettingsPath) ToNotificationsConfig() (notifications.Config, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
return notifications.Config{
|
return notifications.Config{
|
||||||
Enabled: setting.Enabled,
|
Enabled: setting.Enabled,
|
||||||
ServerName: setting.ServerName,
|
EnableRetries: setting.EnableRetries,
|
||||||
|
RetryInterval: uint16(setting.RetryInterval),
|
||||||
|
ServerName: setting.ServerName,
|
||||||
Email: notifications.Email{
|
Email: notifications.Email{
|
||||||
Host: setting.Email.Host,
|
Host: setting.Email.Host,
|
||||||
Port: uint(setting.Email.Port),
|
Port: uint(setting.Email.Port),
|
||||||
@@ -150,30 +152,14 @@ func (o *otherSettingsPath) ToAnalyzerConfig(binaryLocations *binaryLocations) (
|
|||||||
Journalctl: binaryLocations.Journalctl,
|
Journalctl: binaryLocations.Journalctl,
|
||||||
}
|
}
|
||||||
|
|
||||||
login := config.Login{
|
sources, err := setting.ToSources()
|
||||||
Enabled: setting.Login.Enabled,
|
if err != nil {
|
||||||
Notify: setting.Login.Notify,
|
return config.Config{}, err
|
||||||
SSH: config.LoginSSH{
|
|
||||||
Enabled: setting.Login.SSHEnable,
|
|
||||||
Notify: setting.Login.SSHNotify,
|
|
||||||
},
|
|
||||||
Local: config.LoginLocal{
|
|
||||||
Enabled: setting.Login.LocalEnable,
|
|
||||||
Notify: setting.Login.LocalNotify,
|
|
||||||
},
|
|
||||||
Su: config.LoginSu{
|
|
||||||
Enabled: setting.Login.SuEnable,
|
|
||||||
Notify: setting.Login.SuNotify,
|
|
||||||
},
|
|
||||||
Sudo: config.LoginSudo{
|
|
||||||
Enabled: setting.Login.SudoEnable,
|
|
||||||
Notify: setting.Login.SudoNotify,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.Config{
|
return config.Config{
|
||||||
BinPath: binPath,
|
BinPath: binPath,
|
||||||
Login: login,
|
Sources: sources,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type setting struct {
|
|||||||
FallbackLanguage string `mapstructure:"fallback_language"`
|
FallbackLanguage string `mapstructure:"fallback_language"`
|
||||||
PidFile string `mapstructure:"pid_file"`
|
PidFile string `mapstructure:"pid_file"`
|
||||||
SocketFile string `mapstructure:"socket_file"`
|
SocketFile string `mapstructure:"socket_file"`
|
||||||
|
DataDir string `mapstructure:"data_dir"`
|
||||||
|
|
||||||
Log *log
|
Log *log
|
||||||
BinaryLocations *binaryLocations
|
BinaryLocations *binaryLocations
|
||||||
@@ -30,6 +31,7 @@ func settingDefault() *setting {
|
|||||||
FallbackLanguage: "ru",
|
FallbackLanguage: "ru",
|
||||||
PidFile: "/var/run/kor-elf-shield/kor-elf-shield.pid",
|
PidFile: "/var/run/kor-elf-shield/kor-elf-shield.pid",
|
||||||
SocketFile: "/var/run/kor-elf-shield/kor-elf-shield.sock",
|
SocketFile: "/var/run/kor-elf-shield/kor-elf-shield.sock",
|
||||||
|
DataDir: "/var/lib/kor-elf-shield/",
|
||||||
|
|
||||||
Log: logDefault(),
|
Log: logDefault(),
|
||||||
BinaryLocations: binaryLocationsDefault(),
|
BinaryLocations: binaryLocationsDefault(),
|
||||||
@@ -56,6 +58,12 @@ func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, erro
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.DataDir == "" {
|
||||||
|
return daemon.DaemonOptions{}, errors.New(i18n.Lang.T("parameter is not specified", map[string]any{
|
||||||
|
"Parameter": "data_dir",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
|
firewallConfig, err := s.OtherSettingsPath.ToFirewallConfig(dockerSupport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return daemon.DaemonOptions{}, err
|
return daemon.DaemonOptions{}, err
|
||||||
@@ -69,6 +77,7 @@ func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, erro
|
|||||||
return daemon.DaemonOptions{
|
return daemon.DaemonOptions{
|
||||||
PathPidFile: s.PidFile,
|
PathPidFile: s.PidFile,
|
||||||
PathSocketFile: s.SocketFile,
|
PathSocketFile: s.SocketFile,
|
||||||
|
DataDir: s.DataDir,
|
||||||
PathNftables: s.BinaryLocations.Nftables,
|
PathNftables: s.BinaryLocations.Nftables,
|
||||||
ConfigFirewall: firewallConfig,
|
ConfigFirewall: firewallConfig,
|
||||||
ConfigAnalyzer: analyzerConfig,
|
ConfigAnalyzer: analyzerConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user