Add brute force protection notification policy with cooldown and frequency management
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 ¬ificationPolicy{
|
||||
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
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user