Add brute force protection notification policy with cooldown and frequency management

This commit is contained in:
2026-06-17 00:09:19 +05:00
parent 14c4c504fe
commit eda1b956f3
11 changed files with 241 additions and 53 deletions
@@ -8,7 +8,7 @@ import (
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
)
func NewBruteForceProtectionSSH(isNotify bool, group *brute_force_protection.Group) ([]*Source, error) {
func NewBruteForceProtectionSSH(isNotify bool, notifyCooldown int, notifyEvery int, group *brute_force_protection.Group) ([]*Source, error) {
var sources []*Source
journal, err := NewSourceJournal(JournalFieldSystemdUnit, "ssh.service")
@@ -20,9 +20,13 @@ func NewBruteForceProtectionSSH(isNotify bool, group *brute_force_protection.Gro
Type: SourceTypeJournal,
Journal: journal,
BruteForceProtectionRule: &brute_force_protection.Rule{
Name: "_ssh",
Message: i18n.Lang.T("alert.bruteForceProtection.ssh.message"),
IsNotification: isNotify,
Name: "_ssh",
Message: i18n.Lang.T("alert.bruteForceProtection.ssh.message"),
IsNotification: isNotify,
NotificationCooldown: uint32(notifyCooldown),
NotificationEvery: uint32(notifyEvery),
Patterns: []brute_force_protection.RegexPattern{
{
Regexp: regular_expression.NewLazyRegexp(`^Failed password for (\S+) from (\S+) port \S+`),
@@ -3,11 +3,15 @@ package brute_force_protection
import "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/pkg/regular_expression"
type Rule struct {
Name string
Message string
IsNotification bool
Patterns []RegexPattern
Group *Group
Name string
Message string
IsNotification bool
NotificationCooldown uint32
NotificationEvery uint32
Patterns []RegexPattern
Group *Group
}
type RegexPattern struct {
+3
View File
@@ -5,6 +5,7 @@ import (
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/analyzer/log/analysis/alert_group"
analysisBruteForceProtection "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection_group"
"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/geoip"
@@ -33,6 +34,7 @@ func NewAnalysis(
) Analysis {
alertGroupService := alert_group.NewGroup(repositories.AlertGroup(), logger)
bruteForceProtectionGroupService := brute_force_protection_group.NewGroup(repositories.BruteForceProtectionGroup(), logger)
notificationPolicy := analysisBruteForceProtection.NewNotificationPolicy(repositories.BruteForceProtectionNotifyPolicy(), logger)
return &analysis{
alertService: analysisServices.NewAlert(rulesIndex, alertGroupService, logger, notify, ipInfo),
@@ -40,6 +42,7 @@ func NewAnalysis(
rulesIndex,
bruteForceProtectionGroupService,
blockService,
notificationPolicy,
logger,
notify,
ipInfo,
@@ -7,6 +7,7 @@ import (
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
analysisBruteForceProtection "git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/log/analysis/brute_force_protection_group"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/blocking"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/firewall/types"
@@ -23,12 +24,13 @@ type BruteForceProtection interface {
}
type bruteForceProtection struct {
rulesIndex *RulesIndex
groupService brute_force_protection_group.Group
blockService brute_force_protection_group.BlockService
logger log.Logger
notify notifications.Notifications
ipInfo geoip.Info
rulesIndex *RulesIndex
groupService brute_force_protection_group.Group
blockService brute_force_protection_group.BlockService
notificationPolicy analysisBruteForceProtection.NotificationPolicy
logger log.Logger
notify notifications.Notifications
ipInfo geoip.Info
}
type bruteForceProtectionAnalyzeRuleReturn struct {
@@ -53,17 +55,19 @@ func NewBruteForceProtection(
rulesIndex *RulesIndex,
groupService brute_force_protection_group.Group,
blockService brute_force_protection_group.BlockService,
notificationPolicy analysisBruteForceProtection.NotificationPolicy,
logger log.Logger,
notify notifications.Notifications,
ipInfo geoip.Info,
) BruteForceProtection {
return &bruteForceProtection{
rulesIndex: rulesIndex,
groupService: groupService,
blockService: blockService,
logger: logger,
notify: notify,
ipInfo: ipInfo,
rulesIndex: rulesIndex,
groupService: groupService,
blockService: blockService,
notificationPolicy: notificationPolicy,
logger: logger,
notify: notify,
ipInfo: ipInfo,
}
}
@@ -245,7 +249,7 @@ func (p *bruteForceProtection) analyzeRule(rule *brute_force_protection.Rule, me
}
func (p *bruteForceProtection) sendNotifySuccess(notify *bruteForceProtectionNotify) {
if !notify.rule.IsNotification {
if !p.notificationPolicy.IsNotify(notify.rule) {
return
}
@@ -0,0 +1,78 @@
package brute_force_protection
import (
"time"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/analyzer/config/brute_force_protection"
"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"
)
type NotificationPolicy interface {
IsNotify(rule *brute_force_protection.Rule) bool
}
type notificationPolicy struct {
notifyPolicyRepository repository.BruteForceProtectionNotifyPolicyRepository
logger log.Logger
}
func NewNotificationPolicy(notifyPolicyRepository repository.BruteForceProtectionNotifyPolicyRepository, logger log.Logger) NotificationPolicy {
return &notificationPolicy{
notifyPolicyRepository: notifyPolicyRepository,
logger: logger,
}
}
func (s *notificationPolicy) IsNotify(rule *brute_force_protection.Rule) bool {
if !rule.IsNotification {
return false
}
if rule.NotificationCooldown == 0 && rule.NotificationEvery == 0 {
return true
}
isNotify := false
err := s.notifyPolicyRepository.Update(rule.Name, func(notify *entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error) {
if isEvery(rule, notify) {
isNotify = true
return resetNotifyPolicy(rule, notify), nil
}
if time.Now().Unix() >= notify.CooldownTime {
isNotify = true
return resetNotifyPolicy(rule, notify), nil
}
return notify, nil
})
if err != nil {
s.logger.Error(err.Error())
return true
}
return isNotify
}
func isEvery(rule *brute_force_protection.Rule, entity *entity.BruteForceProtectionNotifyPolicy) bool {
if rule.NotificationEvery == 0 {
return false
}
if entity.Every > 0 {
entity.Every--
}
return entity.Every == 0
}
func resetNotifyPolicy(rule *brute_force_protection.Rule, entity *entity.BruteForceProtectionNotifyPolicy) *entity.BruteForceProtectionNotifyPolicy {
entity.Every = rule.NotificationEvery
cooldownTime := time.Now().Add(time.Duration(int(rule.NotificationCooldown)) * time.Second)
entity.CooldownTime = cooldownTime.Unix()
return entity
}
+19 -12
View File
@@ -18,6 +18,7 @@ type Repositories interface {
NotificationsQueue() repository.NotificationsQueueRepository
AlertGroup() repository.AlertGroupRepository
BruteForceProtectionGroup() repository.BruteForceProtectionGroupRepository
BruteForceProtectionNotifyPolicy() repository.BruteForceProtectionNotifyPolicyRepository
Blocking() repository.BlockingRepository
Blocklist() repository.BlocklistRepository
Metadata() repository.MetadataRepository
@@ -26,12 +27,13 @@ type Repositories interface {
}
type repositories struct {
notificationsQueue repository.NotificationsQueueRepository
alertGroup repository.AlertGroupRepository
bruteForceProtectionGroup repository.BruteForceProtectionGroupRepository
blocking repository.BlockingRepository
blocklist repository.BlocklistRepository
metadata repository.MetadataRepository
notificationsQueue repository.NotificationsQueueRepository
alertGroup repository.AlertGroupRepository
bruteForceProtectionGroup repository.BruteForceProtectionGroupRepository
bruteForceProtectionNotifyPolicy repository.BruteForceProtectionNotifyPolicyRepository
blocking repository.BlockingRepository
blocklist repository.BlocklistRepository
metadata repository.MetadataRepository
db []*bbolt.DB
}
@@ -57,12 +59,13 @@ func New(dataDir string) (Repositories, error) {
securityDB, err := bbolt.Open(dataDir+securityDB, 0600, &bbolt.Options{Timeout: 3 * time.Second})
return &repositories{
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
alertGroup: repository.NewAlertGroupRepository(appDB),
bruteForceProtectionGroup: repository.NewBruteForceProtectionGroupRepository(securityDB),
blocking: repository.NewBlockingRepository(securityDB),
blocklist: repository.NewBlocklistRepository(securityDB),
metadata: repository.NewMetadataRepository(appDB),
notificationsQueue: repository.NewNotificationsQueueRepository(appDB),
alertGroup: repository.NewAlertGroupRepository(appDB),
bruteForceProtectionGroup: repository.NewBruteForceProtectionGroupRepository(securityDB),
bruteForceProtectionNotifyPolicy: repository.NewBruteForceProtectionNotifyPolicyRepository(securityDB),
blocking: repository.NewBlockingRepository(securityDB),
blocklist: repository.NewBlocklistRepository(securityDB),
metadata: repository.NewMetadataRepository(appDB),
db: []*bbolt.DB{appDB, securityDB},
}, nil
@@ -80,6 +83,10 @@ func (r *repositories) BruteForceProtectionGroup() repository.BruteForceProtecti
return r.bruteForceProtectionGroup
}
func (r *repositories) BruteForceProtectionNotifyPolicy() repository.BruteForceProtectionNotifyPolicyRepository {
return r.bruteForceProtectionNotifyPolicy
}
func (r *repositories) Blocking() repository.BlockingRepository {
return r.blocking
}
@@ -0,0 +1,6 @@
package entity
type BruteForceProtectionNotifyPolicy struct {
CooldownTime int64
Every uint32
}
@@ -0,0 +1,56 @@
package repository
import (
"encoding/json"
"fmt"
"git.kor-elf.net/kor-elf-shield/kor-elf-shield/internal/daemon/db/entity"
"go.etcd.io/bbolt"
)
type BruteForceProtectionNotifyPolicyRepository interface {
Update(ruleName string, f func(*entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error)) error
}
type bruteForceProtectionNotifyPolicyRepository struct {
db *bbolt.DB
bucket string
}
func NewBruteForceProtectionNotifyPolicyRepository(appDB *bbolt.DB) BruteForceProtectionNotifyPolicyRepository {
return &bruteForceProtectionNotifyPolicyRepository{
db: appDB,
bucket: bruteForceProtectionNotifyPolicyBucket,
}
}
func (r *bruteForceProtectionNotifyPolicyRepository) Update(ruleName string, f func(*entity.BruteForceProtectionNotifyPolicy) (*entity.BruteForceProtectionNotifyPolicy, error)) error {
entityNotify := &entity.BruteForceProtectionNotifyPolicy{}
return r.db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte(r.bucket))
if err != nil {
return err
}
key := []byte(ruleName)
notify := b.Get(key)
if notify != nil {
err = json.Unmarshal(notify, entityNotify)
if err != nil {
return fmt.Errorf("failed to unmarshal brute force protection notify policy: %w", err)
}
}
entityNotify, err = f(entityNotify)
if err != nil {
return err
}
data, err := json.Marshal(entityNotify)
if err != nil {
return err
}
return b.Put(key, data)
})
}
+7 -6
View File
@@ -8,12 +8,13 @@ import (
)
const (
notificationsQueueBucket = "notifications_queue"
alertGroupBucket = "alert_group"
bruteForceProtectionGroupBucket = "brute_force_protection_group"
blockingBucket = "blocking"
blocklistBucket = "blocklist"
metadataBucket = "metadata"
notificationsQueueBucket = "notifications_queue"
alertGroupBucket = "alert_group"
bruteForceProtectionGroupBucket = "brute_force_protection_group"
bruteForceProtectionNotifyPolicyBucket = "brute_force_protection_notify_policy"
blockingBucket = "blocking"
blocklistBucket = "blocklist"
metadataBucket = "metadata"
)
func nextID(b *bbolt.Bucket) ([]byte, error) {
@@ -23,6 +23,8 @@ type BruteForceProtection struct {
BlockingTime int `mapstructure:"blocking_time"`
SSHEnable bool `mapstructure:"ssh_enable"`
SSHNotify bool `mapstructure:"ssh_notify"`
SSHNotifyCooldown int `mapstructure:"ssh_notify_cooldown_seconds"`
SSHNotifyEvery int `mapstructure:"ssh_notify_every"`
SSHGroup string `mapstructure:"ssh_group"`
Groups []BruteForceProtectionGroup
@@ -39,6 +41,8 @@ func defaultBruteForceProtection() BruteForceProtection {
BlockingTime: 3600,
SSHEnable: true,
SSHNotify: true,
SSHNotifyCooldown: 0,
SSHNotifyEvery: 0,
SSHGroup: "",
Groups: []BruteForceProtectionGroup{},
@@ -62,6 +66,15 @@ func (p *BruteForceProtection) Validate() error {
if p.BlockingTime < 0 {
return errors.New("blocking time must be positive")
}
if p.SSHNotifyCooldown < 0 {
return errors.New("ssh notify cooldown must be positive")
}
if p.SSHNotifyEvery < 0 {
return errors.New("ssh notify every must be positive")
}
return nil
}
@@ -85,7 +98,7 @@ func (p *BruteForceProtection) ToSources() ([]*config.Source, error) {
}
sshGroup = p.SSHGroup
}
sshSources, err := config.NewBruteForceProtectionSSH(p.Notify && p.SSHNotify, groups[sshGroup])
sshSources, err := config.NewBruteForceProtectionSSH(p.Notify && p.SSHNotify, p.SSHNotifyCooldown, p.SSHNotifyEvery, groups[sshGroup])
if err != nil {
return nil, err
}
@@ -8,13 +8,15 @@ import (
)
type BruteForceProtectionRule struct {
Enabled bool `mapstructure:"enabled"`
Notify bool `mapstructure:"notify"`
Name string `mapstructure:"name"`
Message string `mapstructure:"message"`
Group string `mapstructure:"group"`
Source Source
Patterns []BruteForceProtectionPattern
Enabled bool `mapstructure:"enabled"`
Notify bool `mapstructure:"notify"`
NotifyCooldown int `mapstructure:"notify_cooldown_seconds"`
NotifyEvery int `mapstructure:"notify_every"`
Name string `mapstructure:"name"`
Message string `mapstructure:"message"`
Group string `mapstructure:"group"`
Source Source
Patterns []BruteForceProtectionPattern
}
func (l *BruteForceProtectionRule) ToSource(isNotify bool, group *brute_force_protection.Group) (*config.Source, error) {
@@ -46,11 +48,13 @@ func (l *BruteForceProtectionRule) ToSource(isNotify bool, group *brute_force_pr
}
source.BruteForceProtectionRule = &brute_force_protection.Rule{
Name: l.Name,
Message: l.Message,
IsNotification: isNotify && l.Notify,
Patterns: patterns,
Group: group,
Name: l.Name,
Message: l.Message,
IsNotification: isNotify && l.Notify,
NotificationCooldown: uint32(l.NotifyCooldown),
NotificationEvery: uint32(l.NotifyEvery),
Patterns: patterns,
Group: group,
}
return source, nil
@@ -65,5 +69,13 @@ func (l *BruteForceProtectionRule) validate() error {
return fmt.Errorf("brute force protection invalid name: %s", l.Name)
}
if l.NotifyCooldown < 0 {
return fmt.Errorf("brute force protection notify cooldown must be positive")
}
if l.NotifyEvery < 0 {
return fmt.Errorf("brute force protection notify every must be positive")
}
return nil
}