-
Notifications
You must be signed in to change notification settings - Fork 3
feat(remediation): support custom remediations #139
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
116ef5b
093fb80
4bca4dc
727b7b5
c9640a5
fffda45
e3737d1
5277f9e
c3ba1d8
04de61b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,37 +1,181 @@ | ||||||
| package remediation | ||||||
|
|
||||||
| // The order matters since we use slices.Max to get the max value | ||||||
| import ( | ||||||
| "sync" | ||||||
| ) | ||||||
|
|
||||||
| // Default weights for built-in remediations | ||||||
| // Allow=0, Unknown=1, then expand others to allow custom remediations to slot in between | ||||||
| const ( | ||||||
| Allow Remediation = iota // Allow remediation | ||||||
| Unknown // Unknown remediation (Unknown is used to have a value for remediation we don't support EG "MFA") | ||||||
| Captcha // Captcha remediation | ||||||
| Ban // Ban remediation | ||||||
| WeightAllow = 0 | ||||||
| WeightUnknown = 1 | ||||||
| WeightCaptcha = 10 | ||||||
| WeightBan = 20 | ||||||
| ) | ||||||
|
|
||||||
| // Remediation represents a remediation type as a string | ||||||
| // We use string pointers for deduplication to reduce allocations | ||||||
| type Remediation struct { | ||||||
| name *string // Pointer to deduplicated string | ||||||
| weight int // Weight for comparison (higher = more severe) | ||||||
| } | ||||||
|
|
||||||
| // registry manages deduplicated remediation strings and their weights | ||||||
| type registry struct { | ||||||
| mu sync.RWMutex | ||||||
| strings map[string]*string // Maps string to its deduplicated pointer | ||||||
| weights map[string]int // Maps remediation name to its weight | ||||||
| } | ||||||
|
|
||||||
| var globalRegistry = ®istry{ | ||||||
| strings: make(map[string]*string), | ||||||
| weights: make(map[string]int), | ||||||
| } | ||||||
|
|
||||||
| // Built-in remediation constants (for convenience) | ||||||
| // Initialized to nil, will be set in init() | ||||||
| var ( | ||||||
| Allow Remediation | ||||||
| Unknown Remediation | ||||||
| Captcha Remediation | ||||||
| Ban Remediation | ||||||
| ) | ||||||
|
|
||||||
| type Remediation uint8 // Remediation type is smallest uint to save space | ||||||
| //nolint:gochecknoinits // init() is required to initialize package-level vars after weights are set | ||||||
| func init() { | ||||||
| // Initialize built-in remediations with default weights | ||||||
| globalRegistry.mu.Lock() | ||||||
| defer globalRegistry.mu.Unlock() | ||||||
|
|
||||||
| // Set weights FIRST before creating strings | ||||||
| globalRegistry.weights["allow"] = WeightAllow | ||||||
| globalRegistry.weights["unknown"] = WeightUnknown | ||||||
| globalRegistry.weights["captcha"] = WeightCaptcha | ||||||
| globalRegistry.weights["ban"] = WeightBan | ||||||
|
|
||||||
| // Pre-create deduplicated strings for built-in remediations | ||||||
| // Must create new string variables for each to avoid pointer aliasing | ||||||
| allowStr := "allow" | ||||||
| unknownStr := "unknown" | ||||||
| captchaStr := "captcha" | ||||||
| banStr := "ban" | ||||||
|
|
||||||
| globalRegistry.strings["allow"] = &allowStr | ||||||
| globalRegistry.strings["unknown"] = &unknownStr | ||||||
| globalRegistry.strings["captcha"] = &captchaStr | ||||||
| globalRegistry.strings["ban"] = &banStr | ||||||
|
|
||||||
| // Now initialize the package-level vars directly (we already hold the lock) | ||||||
| // This avoids deadlock since New() would try to acquire the lock again | ||||||
| Allow = Remediation{name: &allowStr, weight: WeightAllow} | ||||||
| Unknown = Remediation{name: &unknownStr, weight: WeightUnknown} | ||||||
| Captcha = Remediation{name: &captchaStr, weight: WeightCaptcha} | ||||||
| Ban = Remediation{name: &banStr, weight: WeightBan} | ||||||
| } | ||||||
|
|
||||||
| // SetWeight sets a custom weight for a remediation (for configuration) | ||||||
| func SetWeight(name string, weight int) { | ||||||
| globalRegistry.mu.Lock() | ||||||
| defer globalRegistry.mu.Unlock() | ||||||
|
|
||||||
| globalRegistry.weights[name] = weight | ||||||
| // Ensure deduplicated string exists | ||||||
| if _, exists := globalRegistry.strings[name]; !exists { | ||||||
| deduped := name | ||||||
| globalRegistry.strings[name] = &deduped | ||||||
| } | ||||||
| } | ||||||
LaurenceJJones marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| // GetWeight returns the weight for a remediation name | ||||||
| func GetWeight(name string) int { | ||||||
| globalRegistry.mu.RLock() | ||||||
| defer globalRegistry.mu.RUnlock() | ||||||
|
|
||||||
| if weight, exists := globalRegistry.weights[name]; exists { | ||||||
| return weight | ||||||
| } | ||||||
| // Default to Unknown weight for unknown remediations | ||||||
| return WeightUnknown | ||||||
| } | ||||||
|
|
||||||
| // New creates a new Remediation from a string | ||||||
| // Uses deduplicated string pointers to reduce allocations | ||||||
| func New(name string) Remediation { | ||||||
| globalRegistry.mu.Lock() | ||||||
| defer globalRegistry.mu.Unlock() | ||||||
|
|
||||||
| // Get or create deduplicated string pointer | ||||||
| deduped, exists := globalRegistry.strings[name] | ||||||
| if !exists { | ||||||
| // Create new deduplicated string | ||||||
| deduped = &name | ||||||
| globalRegistry.strings[name] = deduped | ||||||
| // Set default weight if not configured | ||||||
| if _, hasWeight := globalRegistry.weights[name]; !hasWeight { | ||||||
| globalRegistry.weights[name] = WeightUnknown | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Read weight from registry (may have been set in init() or SetWeight()) | ||||||
| weight, ok := globalRegistry.weights[name] | ||||||
| if !ok { | ||||||
| // Weight not found, default to Unknown | ||||||
| weight = WeightUnknown | ||||||
| } | ||||||
|
||||||
| } | |
| } |
LaurenceJJones marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
LaurenceJJones marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |||||||||||||||
| "gopkg.in/yaml.v2" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/crowdsecurity/crowdsec-spoa/internal/geo" | ||||||||||||||||
| "github.com/crowdsecurity/crowdsec-spoa/internal/remediation" | ||||||||||||||||
| "github.com/crowdsecurity/crowdsec-spoa/pkg/host" | ||||||||||||||||
| cslogging "github.com/crowdsecurity/crowdsec-spoa/pkg/logging" | ||||||||||||||||
| "github.com/crowdsecurity/go-cs-lib/csyaml" | ||||||||||||||||
|
|
@@ -36,6 +37,11 @@ type BouncerConfig struct { | |||||||||||||||
| ListenUnix string `yaml:"listen_unix"` | ||||||||||||||||
| PrometheusConfig PrometheusConfig `yaml:"prometheus"` | ||||||||||||||||
| PprofConfig PprofConfig `yaml:"pprof"` | ||||||||||||||||
| // RemediationWeights allows users to configure custom weights for remediations | ||||||||||||||||
| // Format: map[string]int where key is remediation name and value is weight | ||||||||||||||||
| // Built-in defaults: allow=0, unknown=1, captcha=10, ban=20 | ||||||||||||||||
| // Custom remediations can slot between these values | ||||||||||||||||
LaurenceJJones marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||
| RemediationWeights map[string]int `yaml:"remediation_weights,omitempty"` | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // MergedConfig() returns the byte content of the patched configuration file (with .yaml.local). | ||||||||||||||||
|
|
@@ -67,6 +73,13 @@ func NewConfig(reader io.Reader) (*BouncerConfig, error) { | |||||||||||||||
| return nil, fmt.Errorf("failed to setup logging: %w", err) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Apply custom remediation weights if configured | ||||||||||||||||
| if config.RemediationWeights != nil { | ||||||||||||||||
| for remediationName, weight := range config.RemediationWeights { | ||||||||||||||||
|
||||||||||||||||
| for remediationName, weight := range config.RemediationWeights { | |
| for remediationName, weight := range config.RemediationWeights { | |
| // Prevent custom weights for built-in remediations to avoid inconsistent behavior | |
| switch remediationName { | |
| case remediation.Ban.Name, remediation.Captcha.Name, remediation.Bypass.Name, remediation.Stream.Name: | |
| return nil, fmt.Errorf("custom weights for built-in remediation '%s' are not supported", remediationName) | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The weight is stored redundantly in both the
Remediationstruct and theglobalRegistry.weightsmap. This creates potential for inconsistency and doesn't align with the claimed "deduplication" benefits.Issues:
SetWeight()is called after aRemediationhas been created, existing instances retain their old weight while new instances get the new weight.Recommendation: Remove the
weightfield from the struct and always look it up from the registry:This ensures consistency and truly achieves deduplication - only the name pointer is stored per instance.