Add TTY login tracking with notification support

This commit is contained in:
2026-01-14 21:51:20 +05:00
parent 5e12b1f6ab
commit ccf228242d
12 changed files with 170 additions and 26 deletions

View File

@@ -45,3 +45,21 @@ 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

View File

@@ -29,7 +29,11 @@ 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")
units = append(units, "_SYSTEMD_UNIT=ssh.service")
}
if config.Login.Enabled && config.Login.Local.Enabled {
units = append(units, "SYSLOG_IDENTIFIER=login")
}
systemdService := analyzerLog.NewSystemd(config.BinPath.Journalctl, units, logger)
@@ -63,14 +67,18 @@ func (a *analyzer) processLogs(ctx context.Context) {
return
}
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 {
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))
}
default:
a.logger.Warn(fmt.Sprintf("Unknown unit: %s", entry.Unit))
a.logger.Debug(fmt.Sprintf("Unknown unit or SyslogIdentifier: %s", entry.Unit))
}
}
}

View File

@@ -4,9 +4,15 @@ type Login struct {
Enabled bool
Notify bool
SSH LoginSSH
Local LoginLocal
}
type LoginSSH struct {
Enabled bool
Notify bool
}
type LoginLocal struct {
Enabled bool
Notify bool
}

View File

@@ -9,10 +9,12 @@ import (
type Analysis interface {
SSH(entry *analysisServices.Entry) error
Locale(entry *analysisServices.Entry) error
}
type analysis struct {
sshService analysisServices.Analysis
sshService analysisServices.Analysis
localeService analysisServices.Analysis
logger log.Logger
notify notifications.Notifications
@@ -20,12 +22,17 @@ type analysis struct {
func NewAnalysis(config *config.Config, logger log.Logger, notify notifications.Notifications) Analysis {
return &analysis{
sshService: analysisServices.NewSSH(config, logger, notify),
logger: logger,
notify: notify,
sshService: analysisServices.NewSSH(config, logger, notify),
localeService: analysisServices.NewLocale(config, logger, notify),
logger: logger,
notify: notify,
}
}
func (a *analysis) SSH(entry *analysisServices.Entry) error {
return a.sshService.Process(entry)
}
func (a *analysis) Locale(entry *analysisServices.Entry) error {
return a.localeService.Process(entry)
}

View File

@@ -9,10 +9,11 @@ type Analysis interface {
}
type Entry struct {
Message string
Unit string
PID string
Time time.Time
Message string
Unit string
PID string
SyslogIdentifier string
Time time.Time
}
type processReturn struct {

View 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
}

View File

@@ -32,6 +32,7 @@ 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"`
}
@@ -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 {
args := []string{"-f", "-n", "0", "-o", "json"}
for _, unit := range s.units {
args = append(args, "-u", unit)
for index, unit := range s.units {
if index > 0 {
args = append(args, "+")
}
args = append(args, unit)
}
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{
Message: raw.Message,
Unit: raw.Unit,
PID: raw.PID,
Time: entryTime,
Message: raw.Message,
Unit: raw.Unit,
PID: raw.PID,
SyslogIdentifier: raw.SyslogIdentifier,
Time: entryTime,
}
}

View File

@@ -28,5 +28,8 @@
"daemon is not reopening logger": "The daemon did not reopen the log",
"alert.login.ssh.subject": "SSH login alert for user {{.User}} from {{.IP}}",
"alert.login.ssh.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}}"
}

View File

@@ -28,5 +28,8 @@
"daemon is not reopening logger": "Жын журналды қайта ашпады",
"alert.login.ssh.subject": "{{.IP}} IP мекенжайынан {{.User}} пайдаланушысына арналған SSH кіру хабарламасы",
"alert.login.ssh.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}}"
}

View File

@@ -28,5 +28,8 @@
"daemon is not reopening logger": "Демон не открыл журнал повторно",
"alert.login.ssh.subject": "SSH-сообщение о входе пользователя {{.User}} с IP-адреса {{.IP}}",
"alert.login.ssh.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}}"
}

View File

@@ -1,18 +1,26 @@
package analyzer
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"`
}
func defaultLogin() Login {
return Login{
Enabled: true,
Notify: true,
Enabled: true,
Notify: true,
SSHEnable: true,
SSHNotify: true,
LocalEnable: true,
LocalNotify: true,
}
}

View File

@@ -157,6 +157,10 @@ func (o *otherSettingsPath) ToAnalyzerConfig(binaryLocations *binaryLocations) (
Enabled: setting.Login.SSHEnable,
Notify: setting.Login.SSHNotify,
},
Local: config.LoginLocal{
Enabled: setting.Login.LocalEnable,
Notify: setting.Login.LocalNotify,
},
}
return config.Config{