Compare commits
19 Commits
b04016c596
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a0cf7bd8a | |||
|
b938b73cfd
|
|||
|
ce031be060
|
|||
|
5e50bc179f
|
|||
|
279f58b644
|
|||
|
26365a519b
|
|||
|
d1f307d2ad
|
|||
|
ccf228242d
|
|||
|
5e12b1f6ab
|
|||
|
67abcc0ef2
|
|||
|
5ad40cdf9b
|
|||
|
374abcea80
|
|||
| 4748630b04 | |||
|
a75df70922
|
|||
|
a84f1ccde6
|
|||
|
0d13f851dd
|
|||
| bbaf0304c3 | |||
| 69157c90cb | |||
| e76d2ae398 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,4 +1,28 @@
|
|||||||
## 0.4.0 (soon)
|
## 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)
|
||||||
***
|
***
|
||||||
#### Русский
|
#### Русский
|
||||||
* Удалён параметр options.docker_support из файла firewall.toml. Настройки от Docker перенесены в файл docker.toml.
|
* Удалён параметр options.docker_support из файла firewall.toml. Настройки от Docker перенесены в файл docker.toml.
|
||||||
@@ -8,6 +32,7 @@
|
|||||||
* Исправлена ошибка:
|
* Исправлена ошибка:
|
||||||
* Настройка binaryLocations.docker не работала.
|
* Настройка binaryLocations.docker не работала.
|
||||||
* Программа аварийно завершалась после остановки Docker'а.
|
* Программа аварийно завершалась после остановки Docker'а.
|
||||||
|
* Указанные в настройках IP-адреса не блокировались во время перенаправления в контейнер Docker.
|
||||||
***
|
***
|
||||||
#### English
|
#### English
|
||||||
* Removed the options.docker_support parameter from firewall.toml. Docker settings have been moved to the docker.toml file.
|
* Removed the options.docker_support parameter from firewall.toml. Docker settings have been moved to the docker.toml file.
|
||||||
@@ -17,6 +42,7 @@
|
|||||||
* Fixed error:
|
* Fixed error:
|
||||||
* The binaryLocations.docker setting did not work.
|
* The binaryLocations.docker setting did not work.
|
||||||
* The program crashed after Docker was stopped.
|
* The program crashed after Docker was stopped.
|
||||||
|
* The IP addresses specified in the settings were not blocked during redirection to the Docker container.
|
||||||
***
|
***
|
||||||
## 0.3.0 (4.1.2026)
|
## 0.3.0 (4.1.2026)
|
||||||
***
|
***
|
||||||
|
|||||||
@@ -45,3 +45,66 @@ ssh_enable = true
|
|||||||
# Default: true
|
# Default: true
|
||||||
###
|
###
|
||||||
ssh_notify = 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
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,23 @@ 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 units []string
|
||||||
if config.Login.Enabled && config.Login.SSH.Enabled {
|
|
||||||
units = append(units, "ssh")
|
if config.Login.Enabled {
|
||||||
|
if config.Login.SSH.Enabled {
|
||||||
|
units = append(units, "_SYSTEMD_UNIT=ssh.service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Login.Local.Enabled {
|
||||||
|
units = append(units, "SYSLOG_IDENTIFIER=login")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, units, logger)
|
||||||
@@ -63,14 +78,26 @@ func (a *analyzer) processLogs(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
|
a.logger.Debug(fmt.Sprintf("Received log entry: %s", entry))
|
||||||
switch entry.Unit {
|
|
||||||
case "ssh.service":
|
switch {
|
||||||
|
case entry.Unit == "ssh.service":
|
||||||
if err := a.analysis.SSH(&entry); err != nil {
|
if err := a.analysis.SSH(&entry); err != nil {
|
||||||
a.logger.Error(fmt.Sprintf("Failed to analyze SSH logs: %s", err))
|
a.logger.Error(fmt.Sprintf("Failed to analyze SSH logs: %s", err))
|
||||||
}
|
}
|
||||||
break
|
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:
|
default:
|
||||||
a.logger.Warn(fmt.Sprintf("Unknown unit: %s", entry.Unit))
|
a.logger.Debug(fmt.Sprintf("Unknown unit or SyslogIdentifier: %s", entry.Unit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,27 @@ type Login struct {
|
|||||||
Enabled bool
|
Enabled bool
|
||||||
Notify bool
|
Notify bool
|
||||||
SSH LoginSSH
|
SSH LoginSSH
|
||||||
|
Local LoginLocal
|
||||||
|
Su LoginSu
|
||||||
|
Sudo LoginSudo
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginSSH struct {
|
type LoginSSH struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Notify bool
|
Notify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginLocal struct {
|
||||||
|
Enabled bool
|
||||||
|
Notify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginSu struct {
|
||||||
|
Enabled bool
|
||||||
|
Notify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginSudo struct {
|
||||||
|
Enabled bool
|
||||||
|
Notify bool
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,16 @@ import (
|
|||||||
|
|
||||||
type Analysis interface {
|
type Analysis interface {
|
||||||
SSH(entry *analysisServices.Entry) error
|
SSH(entry *analysisServices.Entry) error
|
||||||
|
Locale(entry *analysisServices.Entry) error
|
||||||
|
Su(entry *analysisServices.Entry) error
|
||||||
|
Sudo(entry *analysisServices.Entry) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type analysis struct {
|
type analysis struct {
|
||||||
sshService analysisServices.Analysis
|
sshService analysisServices.Analysis
|
||||||
|
localeService analysisServices.Analysis
|
||||||
|
suService analysisServices.Analysis
|
||||||
|
sudoService analysisServices.Analysis
|
||||||
|
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
notify notifications.Notifications
|
notify notifications.Notifications
|
||||||
@@ -20,12 +26,27 @@ type analysis struct {
|
|||||||
|
|
||||||
func NewAnalysis(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
func NewAnalysis(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||||
return &analysis{
|
return &analysis{
|
||||||
sshService: analysisServices.NewSSH(config, logger, notify),
|
sshService: analysisServices.NewSSH(config, logger, notify),
|
||||||
logger: logger,
|
localeService: analysisServices.NewLocale(config, logger, notify),
|
||||||
notify: 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) SSH(entry *analysisServices.Entry) error {
|
||||||
return a.sshService.Process(entry)
|
return a.sshService.Process(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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,17 @@ type Analysis interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
Message string
|
Message string
|
||||||
Unit string
|
Unit string
|
||||||
PID string
|
PID string
|
||||||
Time time.Time
|
SyslogIdentifier string
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type processReturn struct {
|
||||||
|
found bool
|
||||||
|
subject string
|
||||||
|
body string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmptyAnalysis struct{}
|
type EmptyAnalysis struct{}
|
||||||
|
|||||||
78
internal/daemon/analyzer/log/analysis/locale.go
Normal file
78
internal/daemon/analyzer/log/analysis/locale.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -22,12 +22,6 @@ type sshLogin struct {
|
|||||||
notify bool
|
notify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sshProcessReturn struct {
|
|
||||||
found bool
|
|
||||||
subject string
|
|
||||||
body string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSSH(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
func NewSSH(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
|
||||||
if !config.Login.Enabled || !config.Login.SSH.Enabled {
|
if !config.Login.Enabled || !config.Login.SSH.Enabled {
|
||||||
return &EmptyAnalysis{}
|
return &EmptyAnalysis{}
|
||||||
@@ -60,7 +54,7 @@ func (s *ssh) Process(entry *Entry) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *sshLogin) process(entry *Entry) (sshProcessReturn, error) {
|
func (l *sshLogin) process(entry *Entry) (processReturn, error) {
|
||||||
re := regexp.MustCompile(`^Accepted (\S+) for (\S+) from (\S+) port \S+`)
|
re := regexp.MustCompile(`^Accepted (\S+) for (\S+) from (\S+) port \S+`)
|
||||||
matches := re.FindStringSubmatch(entry.Message)
|
matches := re.FindStringSubmatch(entry.Message)
|
||||||
|
|
||||||
@@ -68,13 +62,13 @@ func (l *sshLogin) process(entry *Entry) (sshProcessReturn, error) {
|
|||||||
user := matches[2]
|
user := matches[2]
|
||||||
ip := matches[3]
|
ip := matches[3]
|
||||||
|
|
||||||
return sshProcessReturn{
|
return processReturn{
|
||||||
found: true,
|
found: true,
|
||||||
subject: i18n.Lang.T("alert.login.subject", map[string]any{
|
subject: i18n.Lang.T("alert.login.ssh.subject", map[string]any{
|
||||||
"User": user,
|
"User": user,
|
||||||
"IP": ip,
|
"IP": ip,
|
||||||
}),
|
}),
|
||||||
body: i18n.Lang.T("alert.login.body", map[string]any{
|
body: i18n.Lang.T("alert.login.ssh.body", map[string]any{
|
||||||
"User": user,
|
"User": user,
|
||||||
"IP": ip,
|
"IP": ip,
|
||||||
"Log": entry.Message,
|
"Log": entry.Message,
|
||||||
@@ -83,5 +77,5 @@ func (l *sshLogin) process(entry *Entry) (sshProcessReturn, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshProcessReturn{found: false}, nil
|
return processReturn{found: false}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
81
internal/daemon/analyzer/log/analysis/su.go
Normal file
81
internal/daemon/analyzer/log/analysis/su.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
81
internal/daemon/analyzer/log/analysis/sudo.go
Normal file
81
internal/daemon/analyzer/log/analysis/sudo.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ type journalRawEntry struct {
|
|||||||
Message string `json:"MESSAGE"`
|
Message string `json:"MESSAGE"`
|
||||||
Unit string `json:"_SYSTEMD_UNIT"`
|
Unit string `json:"_SYSTEMD_UNIT"`
|
||||||
PID string `json:"_PID"`
|
PID string `json:"_PID"`
|
||||||
|
SyslogIdentifier string `json:"SYSLOG_IDENTIFIER"`
|
||||||
SourceTimestamp string `json:"_SOURCE_REALTIME_TIMESTAMP"`
|
SourceTimestamp string `json:"_SOURCE_REALTIME_TIMESTAMP"`
|
||||||
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
|
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
|
||||||
}
|
}
|
||||||
@@ -74,8 +75,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 _, unit := range s.units {
|
for index, unit := range s.units {
|
||||||
args = append(args, "-u", unit)
|
if index > 0 {
|
||||||
|
args = append(args, "+")
|
||||||
|
}
|
||||||
|
args = append(args, unit)
|
||||||
}
|
}
|
||||||
cmd := exec.CommandContext(ctx, s.path, args...)
|
cmd := exec.CommandContext(ctx, s.path, args...)
|
||||||
|
|
||||||
@@ -115,10 +119,11 @@ func (s *systemd) watch(ctx context.Context, logChan chan<- analysisServices.Ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
logChan <- analysisServices.Entry{
|
logChan <- analysisServices.Entry{
|
||||||
Message: raw.Message,
|
Message: raw.Message,
|
||||||
Unit: raw.Unit,
|
Unit: raw.Unit,
|
||||||
PID: raw.PID,
|
PID: raw.PID,
|
||||||
Time: entryTime,
|
SyslogIdentifier: raw.SyslogIdentifier,
|
||||||
|
Time: entryTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ type Chains interface {
|
|||||||
NewLocalOutput() error
|
NewLocalOutput() error
|
||||||
LocalOutput() LocalOutput
|
LocalOutput() LocalOutput
|
||||||
|
|
||||||
|
NewLocalForward() error
|
||||||
|
LocalForward() LocalForward
|
||||||
|
|
||||||
ClearRules() error
|
ClearRules() error
|
||||||
|
|
||||||
NewNoneChain(chain string) (Chain, error)
|
NewNoneChain(chain string) (Chain, error)
|
||||||
@@ -39,8 +42,9 @@ type chains struct {
|
|||||||
forward Forward
|
forward Forward
|
||||||
packetFilter PacketFilter
|
packetFilter PacketFilter
|
||||||
|
|
||||||
localInput LocalInput
|
localInput LocalInput
|
||||||
localOutput LocalOutput
|
localOutput LocalOutput
|
||||||
|
localForward LocalForward
|
||||||
|
|
||||||
family nftFamily.Type
|
family nftFamily.Type
|
||||||
table string
|
table string
|
||||||
@@ -147,6 +151,19 @@ func (c *chains) LocalOutput() LocalOutput {
|
|||||||
return c.localOutput
|
return c.localOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *chains) NewLocalForward() error {
|
||||||
|
localForward, err := newLocalForward(c.nft, c.family, c.table)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.localForward = localForward
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chains) LocalForward() LocalForward {
|
||||||
|
return c.localForward
|
||||||
|
}
|
||||||
|
|
||||||
func (c *chains) ClearRules() error {
|
func (c *chains) ClearRules() error {
|
||||||
return clearRules(c.nft, c.family, c.table)
|
return clearRules(c.nft, c.family, c.table)
|
||||||
}
|
}
|
||||||
|
|||||||
41
internal/daemon/firewall/chain/local_forward.go
Normal file
41
internal/daemon/firewall/chain/local_forward.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package chain
|
||||||
|
|
||||||
|
import (
|
||||||
|
nft "git.kor-elf.net/kor-elf-shield/go-nftables-client"
|
||||||
|
nftChain "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain"
|
||||||
|
"git.kor-elf.net/kor-elf-shield/go-nftables-client/family"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalForward interface {
|
||||||
|
AddRule(expr ...string) error
|
||||||
|
AddRuleIn(AddRuleFunc func(expr ...string) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type localForward struct {
|
||||||
|
nft nft.NFT
|
||||||
|
family family.Type
|
||||||
|
table string
|
||||||
|
chain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocalForward(nft nft.NFT, family family.Type, table string) (LocalForward, error) {
|
||||||
|
chain := "local-forward"
|
||||||
|
if err := nft.Chain().Add(family, table, chain, nftChain.TypeNone); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &localForward{
|
||||||
|
nft: nft,
|
||||||
|
family: family,
|
||||||
|
table: table,
|
||||||
|
chain: chain,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localForward) AddRule(expr ...string) error {
|
||||||
|
return l.nft.Rule().Add(l.family, l.table, l.chain, expr...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localForward) AddRuleIn(AddRuleFunc func(expr ...string) error) error {
|
||||||
|
return AddRuleFunc("iifname != \"lo\" counter jump " + l.chain)
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ func (f *firewall) reloadForward() error {
|
|||||||
}
|
}
|
||||||
chain := f.chains.Forward()
|
chain := f.chains.Forward()
|
||||||
|
|
||||||
|
if err := f.reloadForwardAddIPs(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if f.config.Options.DockerSupport {
|
if f.config.Options.DockerSupport {
|
||||||
if err := f.docker.NftChains().ForwardFilterJump(chain.AddRule); err != nil {
|
if err := f.docker.NftChains().ForwardFilterJump(chain.AddRule); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -23,3 +27,53 @@ func (f *firewall) reloadForward() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *firewall) reloadForwardAddIPs() error {
|
||||||
|
if err := f.chains.NewLocalForward(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chain := f.chains.LocalForward()
|
||||||
|
if err := chain.AddRuleIn(f.chains.Forward().AddRule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipConfig := range f.config.IP4.InIPs {
|
||||||
|
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := forwardAddIP(chain.AddRule, ipConfig, "ip"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.config.IP6.Enable {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipConfig := range f.config.IP6.InIPs {
|
||||||
|
if ipConfig.Action != ActionDrop && ipConfig.Action != ActionReject {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := forwardAddIP(chain.AddRule, ipConfig, "ip6"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardAddIP(addRuleFunc func(expr ...string) error, config ConfigIP, ipMatch string) error {
|
||||||
|
rule := ipMatch + " saddr " + config.IP + " iifname != \"lo\""
|
||||||
|
|
||||||
|
// There, during routing, the port changes and then the IP blocking rule will not work.
|
||||||
|
//if !config.OnlyIP {
|
||||||
|
// rule += " " + config.Protocol.String() + " dport " + strconv.Itoa(int(config.Port))
|
||||||
|
//}
|
||||||
|
|
||||||
|
rule += " counter " + config.Action.String()
|
||||||
|
if err := addRuleFunc(rule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,15 @@
|
|||||||
"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.subject": "SSH login alert for user {{.User}} from {{.IP}}",
|
"alert.login.ssh.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}}"
|
"alert.login.ssh.body": "Logged into the OS via ssh:\n Time: {{.Time}}\n IP: {{.IP}}\n User: {{.User}}\n Log: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.locale.subject": "Login message for user {{.User}} (TTY)",
|
||||||
|
"alert.login.locale.body": "Logged into the OS via TTY:\n Time: {{.Time}}\n User: {{.User}}\n Log: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.su.subject": "User {{.ByUser}} has accessed user {{.User}} via su",
|
||||||
|
"alert.login.su.body": "User {{.ByUser}} accessed user {{.User}} via su.\nTime: {{.Time}}\nLog: {{.Log}}",
|
||||||
|
|
||||||
|
"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}}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,15 @@
|
|||||||
"daemon is not running": "Демон жұмыс істемейді",
|
"daemon is not running": "Демон жұмыс істемейді",
|
||||||
"daemon is not reopening logger": "Жын журналды қайта ашпады",
|
"daemon is not reopening logger": "Жын журналды қайта ашпады",
|
||||||
|
|
||||||
"alert.login.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
|
"alert.login.ssh.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
|
||||||
"alert.login.body": "ОС-қа ssh арқылы кірді:\n Уақыт: {{.Time}}\n IP: {{.IP}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}"
|
"alert.login.ssh.body": "ОС-қа ssh арқылы кірді:\n Уақыт: {{.Time}}\n IP: {{.IP}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.locale.subject": "{{.User}} пайдаланушысына арналған кіру хабарламасы (TTY)",
|
||||||
|
"alert.login.locale.body": "ОЖ-ға TTY арқылы кірдіңіз:\n Уақыт: {{.Time}}\n Пайдаланушы: {{.User}}\n Лог: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.su.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кіру мүмкіндігін алды",
|
||||||
|
"alert.login.su.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына su арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.sudo.subject": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кіру мүмкіндігін алды",
|
||||||
|
"alert.login.sudo.body": "{{.ByUser}} пайдаланушысы {{.User}} пайдаланушысына sudo арқылы кірді.\nУақыты: {{.Time}}\nЛог: {{.Log}}"
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,15 @@
|
|||||||
"daemon is not running": "Демон не запущен",
|
"daemon is not running": "Демон не запущен",
|
||||||
"daemon is not reopening logger": "Демон не открыл журнал повторно",
|
"daemon is not reopening logger": "Демон не открыл журнал повторно",
|
||||||
|
|
||||||
"alert.login.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
|
"alert.login.ssh.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
|
||||||
"alert.login.body": "Вошли в ОС через ssh:\n Время: {{.Time}}\n IP: {{.IP}}\n Пользователь: {{.User}}\n Лог: {{.Log}}"
|
"alert.login.ssh.body": "Вошли в ОС через ssh:\n Время: {{.Time}}\n IP: {{.IP}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.locale.subject": "Сообщение о входе пользователя {{.User}} (TTY)",
|
||||||
|
"alert.login.locale.body": "Вошли в ОС через TTY:\n Время: {{.Time}}\n Пользователь: {{.User}}\n Лог: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.su.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su",
|
||||||
|
"alert.login.su.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через su.\nВремя: {{.Time}}\nЛог: {{.Log}}",
|
||||||
|
|
||||||
|
"alert.login.sudo.subject": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo",
|
||||||
|
"alert.login.sudo.body": "Пользователь {{.ByUser}} получил доступ к пользователю {{.User}} через sudo.\nВремя: {{.Time}}\nЛог: {{.Log}}"
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,38 @@
|
|||||||
package analyzer
|
package analyzer
|
||||||
|
|
||||||
type Login struct {
|
type Login struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `mapstructure:"enabled"`
|
||||||
Notify bool `mapstructure:"notify"`
|
Notify bool `mapstructure:"notify"`
|
||||||
|
|
||||||
SSHEnable bool `mapstructure:"ssh_enable"`
|
SSHEnable bool `mapstructure:"ssh_enable"`
|
||||||
SSHNotify bool `mapstructure:"ssh_notify"`
|
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 {
|
func defaultLogin() Login {
|
||||||
return Login{
|
return Login{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Notify: true,
|
Notify: true,
|
||||||
|
|
||||||
SSHEnable: true,
|
SSHEnable: true,
|
||||||
SSHNotify: true,
|
SSHNotify: true,
|
||||||
|
|
||||||
|
LocalEnable: true,
|
||||||
|
LocalNotify: true,
|
||||||
|
|
||||||
|
SuEnable: true,
|
||||||
|
SuNotify: true,
|
||||||
|
|
||||||
|
SudoEnable: false,
|
||||||
|
SudoNotify: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,18 @@ func (o *otherSettingsPath) ToAnalyzerConfig(binaryLocations *binaryLocations) (
|
|||||||
Enabled: setting.Login.SSHEnable,
|
Enabled: setting.Login.SSHEnable,
|
||||||
Notify: setting.Login.SSHNotify,
|
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{
|
||||||
|
|||||||
Reference in New Issue
Block a user