Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bbedc5088 | |||
|
960494eec0
|
|||
|
98a62b4551
|
|||
|
0fa8d88479
|
|||
|
9eef81d1a5
|
|||
|
6821924c8e
|
|||
|
f0958a340f
|
|||
|
d9a40c620c
|
|||
|
fd764fb5c5
|
|||
|
d6af8a7ea5
|
|||
|
f0d5b597cb
|
|||
|
81a28bf485
|
|||
|
0fb8c0b42d
|
|||
|
6b79928b3a
|
|||
| 9a0cf7bd8a | |||
|
b938b73cfd
|
|||
|
ce031be060
|
|||
|
5e50bc179f
|
|||
|
279f58b644
|
|||
|
26365a519b
|
|||
|
d1f307d2ad
|
|||
|
ccf228242d
|
|||
|
5e12b1f6ab
|
|||
|
67abcc0ef2
|
|||
|
5ad40cdf9b
|
|||
|
374abcea80
|
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,3 +1,49 @@
|
||||
## 0.6.0 (8.2.2026)
|
||||
***
|
||||
#### Русский
|
||||
* Добавлена возможность повторной отправки уведомления, если в прошлый раз произошла ошибка.
|
||||
* Добавлена команда `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)
|
||||
***
|
||||
#### Русский
|
||||
* В настройках analyzer.toml добавил параметры local_enable и local_notify.
|
||||
* local_enable = Включает отслеживание локальных авторизаций (TTY, физический доступ). По умолчанию включён.
|
||||
* local_notify = Включает уведомления о локальных авторизациях. По умолчанию включён.
|
||||
* В настройках analyzer.toml добавил параметры su_enable и su_notify.
|
||||
* su_enable = Включает отслеживание авторизаций через su. По умолчанию включён.
|
||||
* su_notify = Включает уведомления об авторизациях через su. По умолчанию включён.
|
||||
* В настройках analyzer.toml добавил параметры sudo_enable и sudo_notify.
|
||||
* sudo_enable = Включает отслеживание авторизаций через sudo. По умолчанию выключен.
|
||||
* sudo_notify = Включает уведомления об авторизациях через sudo. По умолчанию включён.
|
||||
***
|
||||
#### English
|
||||
* Added local_enable and local_notify parameters to analyzer.toml settings.
|
||||
* local_enable = Enables tracking of local logins (TTY, physical access). Enabled by default.
|
||||
* local_notify = Enables notifications about local logins. Enabled by default.
|
||||
* Added su_enable and su_notify parameters to analyzer.toml settings.
|
||||
* su_enable = Enables tracking of logins via su. Enabled by default.
|
||||
* su_notify = Enables notifications about logins via su. Enabled by default.
|
||||
* Added sudo_enable and sudo_notify parameters to analyzer.toml settings.
|
||||
* sudo_enable = Enables tracking of logins via sudo. Off by default.
|
||||
* sudo_notify = Enables notifications about logins via sudo. Enabled by default.
|
||||
***
|
||||
## 0.4.0 (11.1.2026)
|
||||
***
|
||||
#### Русский
|
||||
|
||||
@@ -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
|
||||
|
||||
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/yamlprivate.go
|
||||
|
||||
Copyright 2006-2011 - Kirill Simonov
|
||||
Copyright 2006-2010 Kirill Simonov
|
||||
https://opensource.org/license/mit
|
||||
|
||||
All the remaining project files are covered by the Apache license:
|
||||
|
||||
Copyright 2011-2019 - Canonical Ltd
|
||||
Copyright 2025 - The go-yaml Project Contributors
|
||||
Copyright 2011-2019 Canonical Ltd
|
||||
Copyright 2025 The go-yaml Project Contributors
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* Linux 5.2+
|
||||
* nftables
|
||||
* Systemd
|
||||
* journalctl
|
||||
|
||||
***
|
||||
|
||||
|
||||
@@ -45,3 +45,66 @@ ssh_enable = true
|
||||
# Default: true
|
||||
###
|
||||
ssh_notify = true
|
||||
|
||||
###
|
||||
# Включает отслеживание локальных авторизаций (TTY, физический доступ).
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables tracking of local authorizations (TTY, physical access).
|
||||
# Default: true
|
||||
###
|
||||
local_enable = true
|
||||
|
||||
###
|
||||
# Включает уведомления о локальных авторизациях.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables local authorization notifications.
|
||||
# Default: true
|
||||
###
|
||||
local_notify = true
|
||||
|
||||
###
|
||||
# Включает отслеживание, если кто-либо использует команду `su` для доступа к другой учетной записи.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables tracking if someone uses the `su` command to access another account.
|
||||
# Default: true
|
||||
###
|
||||
su_enable = true
|
||||
|
||||
###
|
||||
# Включает уведомления, если кто-либо использует команду `su` для доступа к другой учетной записи.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications if someone uses the `su` command to access another account.
|
||||
# Default: true
|
||||
###
|
||||
su_notify = true
|
||||
|
||||
###
|
||||
# Включает отслеживание, если кто-либо использует команду `sudo` для доступа к другой учетной записи.
|
||||
#
|
||||
# ПРИМЕЧАНИЕ: Эта опция может стать обременительной, если команда sudo широко используется
|
||||
# для получения root-доступа администраторами или панелями управления.
|
||||
#
|
||||
# По умолчанию: false
|
||||
# ***
|
||||
# Enables tracking if someone uses the `sudo` command to access another account.
|
||||
#
|
||||
# NOTE: This option could become onerous if sudo is used extensively for root
|
||||
# access by administrators or control panels.
|
||||
#
|
||||
# Default: false
|
||||
###
|
||||
sudo_enable = false
|
||||
|
||||
###
|
||||
# Включает уведомления, если кто-либо использует команду `sudo` для доступа к другой учетной записи.
|
||||
# По умолчанию: true
|
||||
# ***
|
||||
# Enables notifications if someone uses the `sudo` command to access another account.
|
||||
# Default: true
|
||||
###
|
||||
sudo_notify = true
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
testing = true
|
||||
|
||||
###
|
||||
# Тестовый период, по истечении которого брандмауэр удалит правила и демон завершит работу.
|
||||
# Тестовый период, по истечении которого брандмауэр удалит правила, очистит другие данные и демон завершит работу.
|
||||
# Период указывается в минутах.
|
||||
# Мин: 1
|
||||
# Макс: 30000
|
||||
# По умолчанию: 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.
|
||||
# Min: 1
|
||||
# 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"
|
||||
|
||||
###
|
||||
# Каталог для постоянных данных приложения (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
|
||||
# ***
|
||||
|
||||
@@ -21,6 +21,35 @@
|
||||
###
|
||||
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
|
||||
|
||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/urfave/cli/v3 v3.4.1
|
||||
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
|
||||
|
||||
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/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=
|
||||
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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.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/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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"
|
||||
|
||||
"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/notifications"
|
||||
"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
|
||||
}
|
||||
|
||||
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 {
|
||||
logger.Fatal(err.Error())
|
||||
|
||||
@@ -81,13 +95,13 @@ func runDaemon(ctx context.Context, _ *cli.Command) error {
|
||||
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()
|
||||
if err != nil {
|
||||
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) {
|
||||
|
||||
@@ -38,6 +38,7 @@ func NewMainApp(appVer AppVersion, defaultConfigPath string) *cli.Command {
|
||||
daemon.CmdStop(),
|
||||
daemon.CmdStatus(),
|
||||
daemon.CmdReopenLogger(),
|
||||
daemon.CmdNotifications(),
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
@@ -27,13 +27,27 @@ type analyzer struct {
|
||||
}
|
||||
|
||||
func New(config config2.Config, logger log.Logger, notify notifications.Notifications) Analyzer {
|
||||
var units []string
|
||||
if config.Login.Enabled && config.Login.SSH.Enabled {
|
||||
units = append(units, "ssh")
|
||||
var matches []string
|
||||
alertRuleIndex := analysisServices.NewAlertRuleIndex()
|
||||
|
||||
for _, source := range config.Sources {
|
||||
switch source.Type {
|
||||
case config2.SourceTypeJournal:
|
||||
match := source.Journal.JournalctlMatch()
|
||||
matches = append(matches, match)
|
||||
default:
|
||||
logger.Error(fmt.Sprintf("Unknown source type: %s", source.Type))
|
||||
continue
|
||||
}
|
||||
|
||||
err := alertRuleIndex.Add(source)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add alert rule: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, units, logger)
|
||||
analysisService := analyzerLog.NewAnalysis(&config, logger, notify)
|
||||
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, matches, logger)
|
||||
analysisService := analyzerLog.NewAnalysis(alertRuleIndex, logger, notify)
|
||||
|
||||
return &analyzer{
|
||||
config: config,
|
||||
@@ -62,16 +76,9 @@ func (a *analyzer) processLogs(ctx context.Context) {
|
||||
// Channel closed
|
||||
return
|
||||
}
|
||||
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
|
||||
switch entry.Unit {
|
||||
case "ssh.service":
|
||||
if err := a.analysis.SSH(&entry); err != nil {
|
||||
a.logger.Error(fmt.Sprintf("Failed to analyze SSH logs: %s", err))
|
||||
}
|
||||
break
|
||||
default:
|
||||
a.logger.Warn(fmt.Sprintf("Unknown unit: %s", entry.Unit))
|
||||
}
|
||||
a.logger.Debug(fmt.Sprintf("Received log entry: %v", entry))
|
||||
|
||||
a.analysis.Alert(&entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,90 @@
|
||||
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 {
|
||||
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,12 +1,148 @@
|
||||
package config
|
||||
|
||||
type Login struct {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
SSH LoginSSH
|
||||
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/i18n"
|
||||
|
||||
func NewLoginSSH(isNotify bool) []*Source {
|
||||
var sources []*Source
|
||||
|
||||
source := &Source{
|
||||
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 {
|
||||
Enabled bool
|
||||
Notify bool
|
||||
func NewLoginLocal(isNotify bool) []*Source {
|
||||
var sources []*Source
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func NewLoginSu(isNotify bool) []*Source {
|
||||
var sources []*Source
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func NewLoginSudo(isNotify bool) []*Source {
|
||||
var sources []*Source
|
||||
|
||||
source := &Source{
|
||||
Type: SourceTypeJournal,
|
||||
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,31 +1,25 @@
|
||||
package log
|
||||
|
||||
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"
|
||||
"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 {
|
||||
SSH(entry *analysisServices.Entry) error
|
||||
Alert(entry *analysisServices.Entry)
|
||||
}
|
||||
|
||||
type analysis struct {
|
||||
sshService analysisServices.Analysis
|
||||
|
||||
logger log.Logger
|
||||
notify notifications.Notifications
|
||||
alertService analysisServices.Alert
|
||||
}
|
||||
|
||||
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{
|
||||
sshService: analysisServices.NewSSH(config, logger, notify),
|
||||
logger: logger,
|
||||
notify: notify,
|
||||
alertService: analysisServices.NewAlert(alertRuleIndex, logger, notify),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analysis) SSH(entry *analysisServices.Entry) error {
|
||||
return a.sshService.Process(entry)
|
||||
func (a *analysis) Alert(entry *analysisServices.Entry) {
|
||||
a.alertService.Analyze(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,22 +1,37 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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 {
|
||||
Message string
|
||||
Unit string
|
||||
PID string
|
||||
Time time.Time
|
||||
Source config.SourceType
|
||||
Message string
|
||||
Unit string
|
||||
PID string
|
||||
SyslogIdentifier string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
type EmptyAnalysis struct{}
|
||||
|
||||
func (empty *EmptyAnalysis) Process(_ *Entry) error {
|
||||
return nil
|
||||
type regexField struct {
|
||||
name string
|
||||
value string
|
||||
}
|
||||
|
||||
func getValueStartEndByRegexIndex(valueId int, idx []int) (start int, end int, err error) {
|
||||
id := 2 * valueId
|
||||
|
||||
if idx == nil || len(idx) <= id+1 {
|
||||
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
|
||||
}
|
||||
|
||||
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,87 +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
|
||||
}
|
||||
|
||||
type sshProcessReturn struct {
|
||||
found bool
|
||||
subject string
|
||||
body string
|
||||
}
|
||||
|
||||
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) (sshProcessReturn, 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 sshProcessReturn{
|
||||
found: true,
|
||||
subject: i18n.Lang.T("alert.login.subject", map[string]any{
|
||||
"User": user,
|
||||
"IP": ip,
|
||||
}),
|
||||
body: i18n.Lang.T("alert.login.body", map[string]any{
|
||||
"User": user,
|
||||
"IP": ip,
|
||||
"Log": entry.Message,
|
||||
"Time": entry.Time,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return sshProcessReturn{found: false}, nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"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/log"
|
||||
)
|
||||
@@ -20,9 +21,9 @@ type Systemd interface {
|
||||
}
|
||||
|
||||
type systemd struct {
|
||||
path string
|
||||
units []string
|
||||
logger log.Logger
|
||||
path string
|
||||
matches []string
|
||||
logger log.Logger
|
||||
|
||||
cmd *exec.Cmd
|
||||
mu sync.Mutex
|
||||
@@ -32,21 +33,22 @@ type journalRawEntry struct {
|
||||
Message string `json:"MESSAGE"`
|
||||
Unit string `json:"_SYSTEMD_UNIT"`
|
||||
PID string `json:"_PID"`
|
||||
SyslogIdentifier string `json:"SYSLOG_IDENTIFIER"`
|
||||
SourceTimestamp string `json:"_SOURCE_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{
|
||||
path: path,
|
||||
units: units,
|
||||
logger: logger,
|
||||
path: path,
|
||||
matches: matches,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *systemd) Run(ctx context.Context, logChan chan<- analysisServices.Entry) {
|
||||
if len(s.units) == 0 {
|
||||
s.logger.Debug("No units specified for journalctl")
|
||||
if len(s.matches) == 0 {
|
||||
s.logger.Debug("No matches specified for journalctl")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,8 +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 {
|
||||
args := []string{"-f", "-n", "0", "-o", "json"}
|
||||
for _, unit := range s.units {
|
||||
args = append(args, "-u", unit)
|
||||
for index, match := range s.matches {
|
||||
if index > 0 {
|
||||
args = append(args, "+")
|
||||
}
|
||||
args = append(args, match)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, s.path, args...)
|
||||
|
||||
@@ -115,10 +120,12 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
||||
}
|
||||
|
||||
logChan <- analysisServices.Entry{
|
||||
Message: raw.Message,
|
||||
Unit: raw.Unit,
|
||||
PID: raw.PID,
|
||||
Time: entryTime,
|
||||
Source: config.SourceTypeJournal,
|
||||
Message: raw.Message,
|
||||
Unit: raw.Unit,
|
||||
PID: raw.PID,
|
||||
SyslogIdentifier: raw.SyslogIdentifier,
|
||||
Time: entryTime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +133,7 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
||||
}
|
||||
|
||||
func (s *systemd) Close() error {
|
||||
if s.units == nil {
|
||||
if s.matches == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package daemon
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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
|
||||
case <-stopTestingCh:
|
||||
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()
|
||||
return
|
||||
case <-d.stopCh:
|
||||
@@ -126,6 +132,15 @@ func (d *daemon) socketCommand(command string, socket socket.Connect) error {
|
||||
return err
|
||||
}
|
||||
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:
|
||||
_ = socket.Write("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 {
|
||||
Enabled bool
|
||||
ServerName string
|
||||
Email Email
|
||||
Enabled bool
|
||||
EnableRetries bool
|
||||
RetryInterval uint16
|
||||
ServerName string
|
||||
Email Email
|
||||
}
|
||||
|
||||
type Email struct {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"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/log"
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
@@ -19,21 +21,26 @@ type Message struct {
|
||||
type Notifications interface {
|
||||
Run()
|
||||
SendAsync(message Message)
|
||||
// DBQueueSize - return size of notifications queue in db
|
||||
DBQueueSize() int
|
||||
DBQueueClear() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type notifications struct {
|
||||
config Config
|
||||
logger log.Logger
|
||||
msgQueue chan Message
|
||||
wg sync.WaitGroup
|
||||
config Config
|
||||
queueRepository repository.NotificationsQueueRepository
|
||||
logger log.Logger
|
||||
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{
|
||||
config: config,
|
||||
logger: logger,
|
||||
msgQueue: make(chan Message, 100),
|
||||
config: config,
|
||||
queueRepository: queueRepository,
|
||||
logger: logger,
|
||||
msgQueue: make(chan Message, 100),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +52,46 @@ func (n *notifications) Run() {
|
||||
n.wg.Add(1)
|
||||
go func() {
|
||||
defer n.wg.Done()
|
||||
for msg := range n.msgQueue {
|
||||
err := n.sendEmail(msg)
|
||||
if err != nil {
|
||||
n.logger.Error(fmt.Sprintf("failed to send email: %v", err))
|
||||
} else if n.config.Enabled {
|
||||
n.logger.Debug(fmt.Sprintf("email sent: Subject %s, Body %s", msg.Subject, msg.Body))
|
||||
|
||||
ticker := time.NewTicker(time.Duration(n.config.RetryInterval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
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:
|
||||
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 {
|
||||
close(n.msgQueue)
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
options := []mail.Option{
|
||||
mail.WithPort(int(config.Port)),
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
type DaemonOptions struct {
|
||||
PathPidFile string
|
||||
PathSocketFile string
|
||||
DataDir string
|
||||
PathNftables string
|
||||
ConfigFirewall firewall.Config
|
||||
ConfigAnalyzer config.Config
|
||||
|
||||
@@ -15,6 +15,16 @@
|
||||
"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.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",
|
||||
"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}}",
|
||||
@@ -27,6 +37,14 @@
|
||||
"daemon is not running": "Daemon is not running",
|
||||
"daemon is not reopening logger": "The daemon did not reopen the log",
|
||||
|
||||
"alert.login.subject": "SSH login alert for user {{.User}} from {{.IP}}",
|
||||
"alert.login.body": "Logged into the OS via ssh:\n Time: {{.Time}}\n IP: {{.IP}}\n User: {{.User}}\n Log: {{.Log}}"
|
||||
"time": "Time: {{.Time}}",
|
||||
"log": "Log: ",
|
||||
"user": "User",
|
||||
"access to user has been gained": "Access to user has been gained",
|
||||
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@
|
||||
"cmd.daemon.reopen_logger.Usage": "Файлды тіркеу үшін қайта ашыңыз",
|
||||
"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": "Командалық қате",
|
||||
"invalid log level": "Параметрлерде көрсетілген журнал деңгейі жарамсыз. Ол қазір мына күйге орнатылған: {{.Level}}. Жарамды мәндер: {{.Levels}}",
|
||||
"invalid log encoding": "Жарамсыз кодтау параметрі. Қазіргі уақытта орнатылған: {{.Encoding}}. Жарамды мәндер: {{.Encodings}}",
|
||||
@@ -27,6 +37,14 @@
|
||||
"daemon is not running": "Демон жұмыс істемейді",
|
||||
"daemon is not reopening logger": "Жын журналды қайта ашпады",
|
||||
|
||||
"alert.login.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
|
||||
"alert.login.body": "ОС-қа ssh арқылы кірді:\n Уақыт: {{.Time}}\n IP: {{.IP}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}"
|
||||
"time": "Уақыт: {{.Time}}",
|
||||
"log": "Лог: ",
|
||||
"user": "Пайдаланушы",
|
||||
"access to user has been gained": "Пайдаланушыға кіру мүмкіндігі алынды",
|
||||
|
||||
"alert.subject": "Ескерту анықталды ({{.Name}}) (топ:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "ОС-қа ssh арқылы кірді.",
|
||||
"alert.login.local.message": "ОЖ-ға TTY арқылы кірдіңіз.",
|
||||
"alert.login.su.message": "su арқылы басқа пайдаланушыға кіру мүмкіндігі алынды.",
|
||||
"alert.login.sudo.message": "sudo арқылы басқа пайдаланушыға кіру мүмкіндігі алынды."
|
||||
}
|
||||
@@ -15,6 +15,16 @@
|
||||
"cmd.daemon.reopen_logger.Usage": "Переоткрыть файл для логирования",
|
||||
"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": "Ошибка команды",
|
||||
"invalid log level": "В настройках указан не верный уровень log. Сейчас указан: {{.Level}}. Допустимые значения: {{.Levels}}",
|
||||
"invalid log encoding": "Неверная настройка encoding. Сейчас указан: {{.Encoding}}. Допустимые значения: {{.Encodings}}",
|
||||
@@ -27,6 +37,14 @@
|
||||
"daemon is not running": "Демон не запущен",
|
||||
"daemon is not reopening logger": "Демон не открыл журнал повторно",
|
||||
|
||||
"alert.login.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
|
||||
"alert.login.body": "Вошли в ОС через ssh:\n Время: {{.Time}}\n IP: {{.IP}}\n Пользователь: {{.User}}\n Лог: {{.Log}}"
|
||||
"time": "Время: {{.Time}}",
|
||||
"log": "Лог: ",
|
||||
"user": "Пользователь",
|
||||
"access to user has been gained": "Получен доступ к пользователю",
|
||||
|
||||
"alert.subject": "Обнаружено оповещение ({{.Name}}) (группа:{{.GroupName}})",
|
||||
"alert.login.ssh.message": "Вошли в ОС через ssh.",
|
||||
"alert.login.local.message": "Вошли в ОС через TTY.",
|
||||
"alert.login.su.message": "Получили доступ к другому пользователю через su.",
|
||||
"alert.login.sudo.message": "Получили доступ к другому пользователю через sudo."
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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/setting/validate"
|
||||
"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 {
|
||||
if err := s.Login.Validate(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,21 +1,71 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config"
|
||||
)
|
||||
|
||||
type Login struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Notify bool `mapstructure:"notify"`
|
||||
|
||||
SSHEnable bool `mapstructure:"ssh_enable"`
|
||||
SSHNotify bool `mapstructure:"ssh_notify"`
|
||||
|
||||
LocalEnable bool `mapstructure:"local_enable"`
|
||||
LocalNotify bool `mapstructure:"local_notify"`
|
||||
|
||||
SuEnable bool `mapstructure:"su_enable"`
|
||||
SuNotify bool `mapstructure:"su_notify"`
|
||||
|
||||
SudoEnable bool `mapstructure:"sudo_enable"`
|
||||
SudoNotify bool `mapstructure:"sudo_notify"`
|
||||
}
|
||||
|
||||
func defaultLogin() Login {
|
||||
return Login{
|
||||
Enabled: true,
|
||||
Notify: true,
|
||||
Enabled: true,
|
||||
Notify: true,
|
||||
|
||||
SSHEnable: true,
|
||||
SSHNotify: true,
|
||||
|
||||
LocalEnable: true,
|
||||
LocalNotify: true,
|
||||
|
||||
SuEnable: true,
|
||||
SuNotify: true,
|
||||
|
||||
SudoEnable: false,
|
||||
SudoNotify: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (l Login) Validate() error {
|
||||
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 {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
ServerName string `mapstructure:"server_name"`
|
||||
Email Email
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
EnableRetries bool `mapstructure:"enable_retries"`
|
||||
RetryInterval int16 `mapstructure:"retry_interval"`
|
||||
ServerName string `mapstructure:"server_name"`
|
||||
Email Email
|
||||
}
|
||||
|
||||
func InitSetting(path string) (Setting, error) {
|
||||
@@ -44,9 +46,11 @@ func InitSetting(path string) (Setting, error) {
|
||||
|
||||
func settingDefault() Setting {
|
||||
return Setting{
|
||||
Enabled: false,
|
||||
ServerName: "server",
|
||||
Email: defaultEmail(),
|
||||
Enabled: false,
|
||||
EnableRetries: true,
|
||||
RetryInterval: 600,
|
||||
ServerName: "server",
|
||||
Email: defaultEmail(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,5 +67,9 @@ func (s Setting) Validate() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.RetryInterval < 1 {
|
||||
return errors.New("retry_interval must be greater than 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -115,8 +115,10 @@ func (o *otherSettingsPath) ToNotificationsConfig() (notifications.Config, error
|
||||
}
|
||||
|
||||
return notifications.Config{
|
||||
Enabled: setting.Enabled,
|
||||
ServerName: setting.ServerName,
|
||||
Enabled: setting.Enabled,
|
||||
EnableRetries: setting.EnableRetries,
|
||||
RetryInterval: uint16(setting.RetryInterval),
|
||||
ServerName: setting.ServerName,
|
||||
Email: notifications.Email{
|
||||
Host: setting.Email.Host,
|
||||
Port: uint(setting.Email.Port),
|
||||
@@ -150,18 +152,14 @@ func (o *otherSettingsPath) ToAnalyzerConfig(binaryLocations *binaryLocations) (
|
||||
Journalctl: binaryLocations.Journalctl,
|
||||
}
|
||||
|
||||
login := config.Login{
|
||||
Enabled: setting.Login.Enabled,
|
||||
Notify: setting.Login.Notify,
|
||||
SSH: config.LoginSSH{
|
||||
Enabled: setting.Login.SSHEnable,
|
||||
Notify: setting.Login.SSHNotify,
|
||||
},
|
||||
sources, err := setting.ToSources()
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
|
||||
return config.Config{
|
||||
BinPath: binPath,
|
||||
Login: login,
|
||||
Sources: sources,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ type setting struct {
|
||||
FallbackLanguage string `mapstructure:"fallback_language"`
|
||||
PidFile string `mapstructure:"pid_file"`
|
||||
SocketFile string `mapstructure:"socket_file"`
|
||||
DataDir string `mapstructure:"data_dir"`
|
||||
|
||||
Log *log
|
||||
BinaryLocations *binaryLocations
|
||||
@@ -30,6 +31,7 @@ func settingDefault() *setting {
|
||||
FallbackLanguage: "ru",
|
||||
PidFile: "/var/run/kor-elf-shield/kor-elf-shield.pid",
|
||||
SocketFile: "/var/run/kor-elf-shield/kor-elf-shield.sock",
|
||||
DataDir: "/var/lib/kor-elf-shield/",
|
||||
|
||||
Log: logDefault(),
|
||||
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)
|
||||
if err != nil {
|
||||
return daemon.DaemonOptions{}, err
|
||||
@@ -69,6 +77,7 @@ func (s setting) ToDaemonOptions(dockerSupport bool) (daemon.DaemonOptions, erro
|
||||
return daemon.DaemonOptions{
|
||||
PathPidFile: s.PidFile,
|
||||
PathSocketFile: s.SocketFile,
|
||||
DataDir: s.DataDir,
|
||||
PathNftables: s.BinaryLocations.Nftables,
|
||||
ConfigFirewall: firewallConfig,
|
||||
ConfigAnalyzer: analyzerConfig,
|
||||
|
||||
Reference in New Issue
Block a user