Skip to content
Merged
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
7 changes: 3 additions & 4 deletions plugins/rule-flood-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ Everything is scoped per tenant:
are each below it never add up to a flood between them.
- **Disabling.** The rule is disabled only for the tenant that flooded. Every
other tenant keeps it running.
- **Notifying.** The offending tenant is notified, since the disable applies to
them and the remediation is theirs to apply. The platform tenant receives a
copy so the operator keeps instance-wide visibility — unless it is already
the offending tenant, in which case a single notification is sent.
- **Notifying.** Only the tenant that flooded is notified, since the disable
applies to them and the remediation is theirs to apply. No other tenant hears
about it.

Alerts that carry no tenant are dropped rather than attributed to a default
tenant.
Expand Down
25 changes: 6 additions & 19 deletions plugins/rule-flood-guard/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -15,9 +14,10 @@ import (
"github.com/threatwinds/go-sdk/catcher"
)

// The window is interpolated rather than hardcoded: windowHours is
// configurable, so a fixed "24h" would misreport the period the count covers.
const notificationMessageTemplate = "Correlation rule '%s' generated %d open, un-deduplicated alerts from data source '%s' for tenant '%s' in the last %dh and was automatically disabled for that tenant to prevent alert flooding. If this volume is expected, use an Alert Tag Rule to mark it 'False positive', or add deduplicateBy/groupBy to the rule, then re-enable it."
// Template for the notification message sent to the tenant that flooded. It is
// formatted with the rule name, the number of alerts, the data source, and the
// window in hours.
const notificationMessageTemplate = "Correlation rule '%s' generated %d open, un-deduplicated alerts from data source '%s' in the last %dh and was automatically disabled to prevent alert flooding. If this volume is expected, use an Alert Tag Rule to mark it 'False positive', or add deduplicateBy/groupBy to the rule, then re-enable it."

// tenantHeader scopes every backend call. Without it the middleware treats an
// internal caller as tenantless and the backend falls back to the platform
Expand Down Expand Up @@ -154,20 +154,7 @@ type notifyRequest struct {
Message string `json:"message"`
}

// platformTenant is the operator's tenant. It gets a copy of every flood
// notification so the operator keeps the instance-wide visibility they had
// before the disable became per-tenant.
const platformTenant = "ce66672c-e36d-4761-a8c8-90058fee1a24"

func (c *backendClient) Notify(ctx context.Context, tenantID, message string) error {
err := c.notifyTenant(ctx, tenantID, message)
if tenantID != platformTenant {
err = errors.Join(err, c.notifyTenant(ctx, platformTenant, message))
}
return err
}

func (c *backendClient) notifyTenant(ctx context.Context, tenantID, message string) error {
payload, err := json.Marshal(notifyRequest{Source: "SYSTEM", Type: "WARNING", Message: message})
if err != nil {
return err
Expand Down Expand Up @@ -196,6 +183,6 @@ func (c *backendClient) notifyTenant(ctx context.Context, tenantID, message stri
return nil
}

func floodNotificationMessage(tenantID, ruleName string, count int64, dataSource string, windowHours int) string {
return fmt.Sprintf(notificationMessageTemplate, ruleName, count, dataSource, tenantID, windowHours)
func floodNotificationMessage(ruleName string, count int64, dataSource string, windowHours int) string {
return fmt.Sprintf(notificationMessageTemplate, ruleName, count, dataSource, windowHours)
}
2 changes: 1 addition & 1 deletion plugins/rule-flood-guard/guard.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func evaluateOnce(ctx context.Context, search searchFunc, client disableNotifier
continue
}

msg := floodNotificationMessage(b.TenantID, b.RuleName, b.Count, b.DataSource, cfg.WindowHours)
msg := floodNotificationMessage(b.RuleName, b.Count, b.DataSource, cfg.WindowHours)
if err := client.Notify(ctx, b.TenantID, msg); err != nil {
_ = catcher.Error("rule-flood-guard: failed to send notification", err, map[string]any{
"tenantId": b.TenantID, "ruleName": b.RuleName, "dataSource": b.DataSource,
Expand Down
Loading