Add TTY login tracking with notification support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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}}"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user