Skip to content

Commit 6a9cd68

Browse files
authored
fix(rule-flood-guard): notify only the affected tenant, without naming it (#2496)
The flood notification went to the tenant whose rule was disabled and also to the platform tenant, so the operator would keep the instance-wide visibility they had before the disable became per-tenant. In an MSSP that turns every flood in every tenant into a ping on the operator's bell, which is the same alert fatigue this plugin exists to prevent. The tenant is the one who can act on it, so the copy is gone. With no operator copy left, naming the tenant in the message was noise: the only reader is the affected tenant, and showing them their own UUID tells them nothing. The message now speaks to them directly. The tenant is still in the structured log, which is where operators read from.
1 parent 1aef4bf commit 6a9cd68

3 files changed

Lines changed: 10 additions & 24 deletions

File tree

plugins/rule-flood-guard/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ Everything is scoped per tenant:
1717
are each below it never add up to a flood between them.
1818
- **Disabling.** The rule is disabled only for the tenant that flooded. Every
1919
other tenant keeps it running.
20-
- **Notifying.** The offending tenant is notified, since the disable applies to
21-
them and the remediation is theirs to apply. The platform tenant receives a
22-
copy so the operator keeps instance-wide visibility — unless it is already
23-
the offending tenant, in which case a single notification is sent.
20+
- **Notifying.** Only the tenant that flooded is notified, since the disable
21+
applies to them and the remediation is theirs to apply. No other tenant hears
22+
about it.
2423

2524
Alerts that carry no tenant are dropped rather than attributed to a default
2625
tenant.

plugins/rule-flood-guard/backend.go

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7-
"errors"
87
"fmt"
98
"io"
109
"net/http"
@@ -15,9 +14,10 @@ import (
1514
"github.com/threatwinds/go-sdk/catcher"
1615
)
1716

18-
// The window is interpolated rather than hardcoded: windowHours is
19-
// configurable, so a fixed "24h" would misreport the period the count covers.
20-
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."
17+
// Template for the notification message sent to the tenant that flooded. It is
18+
// formatted with the rule name, the number of alerts, the data source, and the
19+
// window in hours.
20+
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."
2121

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

157-
// platformTenant is the operator's tenant. It gets a copy of every flood
158-
// notification so the operator keeps the instance-wide visibility they had
159-
// before the disable became per-tenant.
160-
const platformTenant = "ce66672c-e36d-4761-a8c8-90058fee1a24"
161-
162157
func (c *backendClient) Notify(ctx context.Context, tenantID, message string) error {
163-
err := c.notifyTenant(ctx, tenantID, message)
164-
if tenantID != platformTenant {
165-
err = errors.Join(err, c.notifyTenant(ctx, platformTenant, message))
166-
}
167-
return err
168-
}
169-
170-
func (c *backendClient) notifyTenant(ctx context.Context, tenantID, message string) error {
171158
payload, err := json.Marshal(notifyRequest{Source: "SYSTEM", Type: "WARNING", Message: message})
172159
if err != nil {
173160
return err
@@ -196,6 +183,6 @@ func (c *backendClient) notifyTenant(ctx context.Context, tenantID, message stri
196183
return nil
197184
}
198185

199-
func floodNotificationMessage(tenantID, ruleName string, count int64, dataSource string, windowHours int) string {
200-
return fmt.Sprintf(notificationMessageTemplate, ruleName, count, dataSource, tenantID, windowHours)
186+
func floodNotificationMessage(ruleName string, count int64, dataSource string, windowHours int) string {
187+
return fmt.Sprintf(notificationMessageTemplate, ruleName, count, dataSource, windowHours)
201188
}

plugins/rule-flood-guard/guard.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func evaluateOnce(ctx context.Context, search searchFunc, client disableNotifier
5050
continue
5151
}
5252

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

0 commit comments

Comments
 (0)