Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/cooldown #371

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/component-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
Test_08_ApplicationProfilePatching,
Test_10_MalwareDetectionTest,
Test_11_EndpointTest,
Test_12_CooldownTest,
# Test_10_DemoTest
# Test_11_DuplicationTest
]
Expand Down
128 changes: 128 additions & 0 deletions pkg/cooldown/cooldown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cooldown

import (
"container/list"
"sync"
"time"

"github.com/goradd/maps"
)

// CooldownConfig holds the configuration for a cooldown
type CooldownConfig struct {
Threshold int
AlertWindow time.Duration
BaseCooldown time.Duration
MaxCooldown time.Duration
CooldownIncrease float64
}

// Cooldown represents the cooldown mechanism for a specific alert
type Cooldown struct {
mu sync.RWMutex
lastAlertTime time.Time
currentCooldown time.Duration
alertTimes *list.List
config CooldownConfig
}

// CooldownManager manages cooldowns for different alerts
type CooldownManager struct {
cooldowns maps.SafeMap[string, *Cooldown]
}

// NewCooldownManager creates a new CooldownManager
func NewCooldownManager() *CooldownManager {
return &CooldownManager{}
}

// NewCooldown creates a new Cooldown with the given configuration
func NewCooldown(config CooldownConfig) *Cooldown {
return &Cooldown{
currentCooldown: config.BaseCooldown,
alertTimes: list.New(),
config: config,
}
}

// ConfigureCooldown sets up or updates the cooldown configuration for a specific alert
func (cm *CooldownManager) ConfigureCooldown(alertID string, config CooldownConfig) {
cooldown := NewCooldown(config)
cm.cooldowns.Set(alertID, cooldown)
}

// ShouldAlert determines if an alert should be triggered based on the cooldown mechanism
func (cm *CooldownManager) ShouldAlert(alertID string) bool {
if !cm.cooldowns.Has(alertID) {
// If no configuration exists, always allow the alert
return true
}

cooldown := cm.cooldowns.Get(alertID)

return cooldown.shouldAlert()
}

func (c *Cooldown) shouldAlert() bool {
c.mu.Lock()
defer c.mu.Unlock()

now := time.Now()

// Remove alerts outside the window
for c.alertTimes.Len() > 0 {
if now.Sub(c.alertTimes.Front().Value.(time.Time)) > c.config.AlertWindow {
c.alertTimes.Remove(c.alertTimes.Front())
} else {
break
}
}

// If we're below the threshold, always allow the alert
if c.alertTimes.Len() < c.config.Threshold {
c.alertTimes.PushBack(now)
c.lastAlertTime = now
return true
}

// If we're at the threshold, allow the alert but increase the cooldown
if c.alertTimes.Len() == c.config.Threshold {
c.alertTimes.PushBack(now)
c.lastAlertTime = now
c.currentCooldown = time.Duration(float64(c.config.BaseCooldown) * c.config.CooldownIncrease)
if c.currentCooldown > c.config.MaxCooldown {
c.currentCooldown = c.config.MaxCooldown
}
return true
}

// If we've exceeded the threshold, check if we're still in the cooldown period
if now.Sub(c.lastAlertTime) < c.currentCooldown {
return false
}

// We're past the cooldown period, allow the alert and increase the cooldown further
c.alertTimes.PushBack(now)
c.lastAlertTime = now
c.currentCooldown = time.Duration(float64(c.currentCooldown) * c.config.CooldownIncrease)
if c.currentCooldown > c.config.MaxCooldown {
c.currentCooldown = c.config.MaxCooldown
}
return true
}

// ResetCooldown resets the cooldown for a specific alert
func (cm *CooldownManager) ResetCooldown(alertID string) {
if cm.cooldowns.Has(alertID) {
cooldown := cm.cooldowns.Get(alertID)
cooldown.mu.Lock()
cooldown.alertTimes.Init() // Clear the list
cooldown.currentCooldown = cooldown.config.BaseCooldown
cooldown.mu.Unlock()
}
}

// HasCooldownConfig checks if a cooldown configuration exists for a specific alert
func (cm *CooldownManager) HasCooldownConfig(alertID string) bool {
return cm.cooldowns.Has(alertID)
}
220 changes: 220 additions & 0 deletions pkg/cooldown/cooldown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package cooldown

import (
"fmt"
"sync"
"testing"
"time"
)

func TestNewCooldownManager(t *testing.T) {
cm := NewCooldownManager()
if cm == nil {
t.Error("NewCooldownManager() returned nil")
}
}

func TestConfigureCooldown(t *testing.T) {
cm := NewCooldownManager()
config := CooldownConfig{
Threshold: 5,
AlertWindow: 100 * time.Millisecond,
BaseCooldown: 10 * time.Millisecond,
MaxCooldown: 500 * time.Millisecond,
CooldownIncrease: 2.0,
}

cm.ConfigureCooldown("test-alert", config)

if !cm.HasCooldownConfig("test-alert") {
t.Error("ConfigureCooldown() did not add the configuration")
}

// Test updating existing configuration
newConfig := CooldownConfig{
Threshold: 10,
AlertWindow: 200 * time.Millisecond,
BaseCooldown: 20 * time.Millisecond,
MaxCooldown: 1 * time.Second,
CooldownIncrease: 3.0,
}
cm.ConfigureCooldown("test-alert", newConfig)

if !cm.HasCooldownConfig("test-alert") {
t.Error("ConfigureCooldown() did not update the configuration")
}
}

func TestComprehensiveShouldAlert(t *testing.T) {
cm := NewCooldownManager()
config := CooldownConfig{
Threshold: 3,
AlertWindow: 100 * time.Millisecond,
BaseCooldown: 10 * time.Millisecond,
MaxCooldown: 50 * time.Millisecond,
CooldownIncrease: 2.0,
}
cm.ConfigureCooldown("test-alert", config)

fmt.Println("Starting comprehensive cooldown test...")
fmt.Printf("Config: Threshold=%d, AlertWindow=%v, BaseCooldown=%v, MaxCooldown=%v, CooldownIncrease=%.1f\n\n",
config.Threshold, config.AlertWindow, config.BaseCooldown, config.MaxCooldown, config.CooldownIncrease)

testCases := []struct {
name string
delay time.Duration
expected bool
}{
{"First alert", 0, true},
{"Second alert (immediate)", 0, true},
{"Third alert (immediate)", 0, true},
{"Fourth alert (immediate, should increase cooldown)", 0, true},
{"Fifth alert (immediate, should be blocked)", 0, false},
{"Sixth alert (after base cooldown)", config.BaseCooldown, false},
{"Seventh alert (after increased cooldown)", config.BaseCooldown * 2, true},
{"Eighth alert (immediate after cooldown)", 0, false},
{"Ninth alert (after alert window)", config.AlertWindow, true},
{"Tenth alert (immediate)", 0, true},
{"Eleventh alert (immediate)", 0, true},
}

startTime := time.Now()

for i, tc := range testCases {
time.Sleep(tc.delay)
result := cm.ShouldAlert("test-alert")
elapsed := time.Since(startTime)

cooldown := cm.cooldowns.Get("test-alert")
fmt.Printf("%d. %s (at %v):\n Expected: %v, Got: %v\n Alert Count: %d, Current Cooldown: %v\n",
i+1, tc.name, elapsed.Round(time.Millisecond), tc.expected, result, cooldown.alertTimes.Len(), cooldown.currentCooldown)

if result != tc.expected {
t.Errorf("%s: expected %v, got %v", tc.name, tc.expected, result)
}
}
}

func TestResetCooldown(t *testing.T) {
cm := NewCooldownManager()
config := CooldownConfig{
Threshold: 3,
AlertWindow: 100 * time.Millisecond,
BaseCooldown: 10 * time.Millisecond,
MaxCooldown: 50 * time.Millisecond,
CooldownIncrease: 2.0,
}
cm.ConfigureCooldown("test-alert", config)

// Trigger alerts to increase cooldown
for i := 0; i < 4; i++ {
cm.ShouldAlert("test-alert")
}

// Verify that cooldown is in effect
if cm.ShouldAlert("test-alert") {
t.Error("Cooldown was not in effect before reset")
}

// Reset cooldown
cm.ResetCooldown("test-alert")

// Allow a small delay for reset to take effect
time.Sleep(1 * time.Millisecond)

// Alert should now be allowed
if !cm.ShouldAlert("test-alert") {
t.Error("Alert was not allowed after reset")
}

// Resetting an unconfigured alert should not panic
cm.ResetCooldown("unconfigured-alert")
}

func TestCooldownIncrease(t *testing.T) {
cm := NewCooldownManager()
config := CooldownConfig{
Threshold: 2,
AlertWindow: 50 * time.Millisecond,
BaseCooldown: 10 * time.Millisecond,
MaxCooldown: 100 * time.Millisecond,
CooldownIncrease: 2.0,
}
cm.ConfigureCooldown("test-alert", config)

// Trigger alerts to increase cooldown
for i := 0; i < 3; i++ {
cm.ShouldAlert("test-alert")
}

// Next alert should be blocked due to increased cooldown
if cm.ShouldAlert("test-alert") {
t.Error("Alert was allowed despite increased cooldown")
}

// Wait for increased cooldown and alert should be allowed
time.Sleep(21 * time.Millisecond)
if !cm.ShouldAlert("test-alert") {
t.Error("Alert was not allowed after increased cooldown period")
}
}

func TestCooldownDecrease(t *testing.T) {
cm := NewCooldownManager()
config := CooldownConfig{
Threshold: 4,
AlertWindow: 50 * time.Millisecond,
BaseCooldown: 10 * time.Millisecond,
MaxCooldown: 100 * time.Millisecond,
CooldownIncrease: 2.0,
}
cm.ConfigureCooldown("test-alert", config)

// Trigger alerts to increase cooldown
for i := 0; i < 5; i++ {
cm.ShouldAlert("test-alert")
}

// Wait for alert window to pass
time.Sleep(51 * time.Millisecond)

// Trigger a single alert
cm.ShouldAlert("test-alert")

// Wait for base cooldown
time.Sleep(11 * time.Millisecond)

// Alert should be allowed and cooldown should have decreased
if !cm.ShouldAlert("test-alert") {
t.Error("Alert was not allowed after cooldown should have decreased")
}
}

func TestConcurrency(t *testing.T) {
cm := NewCooldownManager()
config := CooldownConfig{
Threshold: 5,
AlertWindow: 100 * time.Millisecond,
BaseCooldown: 10 * time.Millisecond,
MaxCooldown: 500 * time.Millisecond,
CooldownIncrease: 2.0,
}
cm.ConfigureCooldown("test-alert", config)

// Run 100 goroutines simultaneously
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cm.ShouldAlert("test-alert")
}()
}

wg.Wait()

// Check that the cooldown has increased
if cm.ShouldAlert("test-alert") {
t.Error("Cooldown did not increase as expected under concurrent load")
}
}
6 changes: 6 additions & 0 deletions pkg/ruleengine/ruleengine_interface.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ruleengine

import (
"github.com/kubescape/node-agent/pkg/cooldown"
"github.com/kubescape/node-agent/pkg/objectcache"
"github.com/kubescape/node-agent/pkg/utils"

Expand Down Expand Up @@ -71,6 +72,9 @@ type RuleEvaluator interface {

// Get rule parameters
GetParameters() map[string]interface{}

// Cooldown configuration
CooldownConfig() *cooldown.CooldownConfig
}

// RuleSpec is an interface for rule requirements
Expand All @@ -92,6 +96,8 @@ type RuleFailure interface {
GetRuntimeAlertK8sDetails() apitypes.RuntimeAlertK8sDetails
// Get Rule ID
GetRuleId() string
// Get Failure identifier
GetFailureIdentifier() string

// Set Workload Details
SetWorkloadDetails(workloadDetails string)
Expand Down
Loading
Loading