44 "bytes"
55 "context"
66 "encoding/json"
7+ "errors"
78 "fmt"
89 "io"
910 "net/http"
@@ -14,7 +15,14 @@ import (
1415 "github.com/threatwinds/go-sdk/catcher"
1516)
1617
17- const notificationMessageTemplate = "Correlation rule '%s' generated over %d open, un-deduplicated alerts from data source '%s' in the last 24h 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."
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."
21+
22+ // tenantHeader scopes every backend call. Without it the middleware treats an
23+ // internal caller as tenantless and the backend falls back to the platform
24+ // tenant, so the disable would land on the wrong tenant's rule list.
25+ const tenantHeader = "X-Tenant-Id"
1826
1927type backendClient struct {
2028 baseURL string
@@ -36,13 +44,14 @@ type ruleSearchResult struct {
3644 RuleActive bool `json:"ruleActive"`
3745}
3846
39- func (c * backendClient ) resolveRule (ctx context.Context , ruleName string ) ([]ruleSearchResult , error ) {
47+ func (c * backendClient ) resolveRule (ctx context.Context , tenantID , ruleName string ) ([]ruleSearchResult , error ) {
4048 endpoint := c .baseURL + "/api/v1/eventprocessing/correlation-rule/search-by-filters?ruleName=" + url .QueryEscape (ruleName )
4149 req , err := http .NewRequestWithContext (ctx , http .MethodGet , endpoint , nil )
4250 if err != nil {
4351 return nil , err
4452 }
4553 req .Header .Set ("X-Internal-Key" , c .internalKey )
54+ req .Header .Set (tenantHeader , tenantID )
4655
4756 resp , err := c .httpClient .Do (req )
4857 if err != nil {
@@ -53,7 +62,7 @@ func (c *backendClient) resolveRule(ctx context.Context, ruleName string) ([]rul
5362 if resp .StatusCode >= 400 {
5463 body , _ := io .ReadAll (resp .Body )
5564 return nil , catcher .Error ("search-by-filters call returned error status" , nil , map [string ]any {
56- "status" : resp .StatusCode , "body" : string (body ), "ruleName" : ruleName ,
65+ "status" : resp .StatusCode , "body" : string (body ), "ruleName" : ruleName , "tenantId" : tenantID ,
5766 })
5867 }
5968
@@ -70,7 +79,7 @@ func (c *backendClient) resolveRule(ctx context.Context, ruleName string) ([]rul
7079 }
7180 if len (matches ) > 1 {
7281 catcher .Warn ("rule-flood-guard: ambiguous rule name collision, disabling every exact match" , map [string ]any {
73- "ruleName" : ruleName , "matches" : len (matches ),
82+ "ruleName" : ruleName , "matches" : len (matches ), "tenantId" : tenantID ,
7483 })
7584 }
7685 return matches , nil
@@ -80,8 +89,8 @@ type activateDeactivateResponse struct {
8089 Changed bool `json:"changed"`
8190}
8291
83- func (c * backendClient ) Deactivate (ctx context.Context , ruleName string ) (bool , error ) {
84- matches , err := c .resolveRule (ctx , ruleName )
92+ func (c * backendClient ) Deactivate (ctx context.Context , tenantID , ruleName string ) (bool , error ) {
93+ matches , err := c .resolveRule (ctx , tenantID , ruleName )
8594 if err != nil {
8695 return false , err
8796 }
@@ -94,7 +103,7 @@ func (c *backendClient) Deactivate(ctx context.Context, ruleName string) (bool,
94103 if ! matches [i ].RuleActive {
95104 continue
96105 }
97- ruleChanged , err := c .deactivateOne (ctx , ruleName , matches [i ].RelPath )
106+ ruleChanged , err := c .deactivateOne (ctx , tenantID , ruleName , matches [i ].RelPath )
98107 if err != nil {
99108 return changed , err
100109 }
@@ -103,19 +112,20 @@ func (c *backendClient) Deactivate(ctx context.Context, ruleName string) (bool,
103112 }
104113 }
105114 catcher .Info ("rule-flood-guard: exact-match rules processed for deactivation" , map [string ]any {
106- "ruleName" : ruleName , "matches" : len (matches ), "changed" : changed ,
115+ "ruleName" : ruleName , "matches" : len (matches ), "changed" : changed , "tenantId" : tenantID ,
107116 })
108117 return changed , nil
109118}
110119
111- func (c * backendClient ) deactivateOne (ctx context.Context , ruleName , relPath string ) (bool , error ) {
120+ func (c * backendClient ) deactivateOne (ctx context.Context , tenantID , ruleName , relPath string ) (bool , error ) {
112121 endpoint := fmt .Sprintf ("%s/api/v1/eventprocessing/correlation-rule/activate-deactivate?relPath=%s&active=false" ,
113122 c .baseURL , url .QueryEscape (relPath ))
114123 req , err := http .NewRequestWithContext (ctx , http .MethodPut , endpoint , nil )
115124 if err != nil {
116125 return false , err
117126 }
118127 req .Header .Set ("X-Internal-Key" , c .internalKey )
128+ req .Header .Set (tenantHeader , tenantID )
119129
120130 resp , err := c .httpClient .Do (req )
121131 if err != nil {
@@ -126,7 +136,8 @@ func (c *backendClient) deactivateOne(ctx context.Context, ruleName, relPath str
126136 if resp .StatusCode >= 400 {
127137 body , _ := io .ReadAll (resp .Body )
128138 return false , catcher .Error ("activate-deactivate call returned error status" , nil , map [string ]any {
129- "status" : resp .StatusCode , "body" : string (body ), "ruleName" : ruleName , "relPath" : relPath ,
139+ "status" : resp .StatusCode , "body" : string (body ), "ruleName" : ruleName ,
140+ "relPath" : relPath , "tenantId" : tenantID ,
130141 })
131142 }
132143
@@ -143,12 +154,20 @@ type notifyRequest struct {
143154 Message string `json:"message"`
144155}
145156
146- // platformTenant receives the flood notifications. A rule that floods is
147- // deactivated for the whole instance and the rules are not a tenant's to
148- // re-enable, so the operator is the one who has to know .
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 .
149160const platformTenant = "ce66672c-e36d-4761-a8c8-90058fee1a24"
150161
151- func (c * backendClient ) Notify (ctx context.Context , message string ) error {
162+ 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 {
152171 payload , err := json .Marshal (notifyRequest {Source : "SYSTEM" , Type : "WARNING" , Message : message })
153172 if err != nil {
154173 return err
@@ -159,7 +178,7 @@ func (c *backendClient) Notify(ctx context.Context, message string) error {
159178 return err
160179 }
161180 req .Header .Set ("X-Internal-Key" , c .internalKey )
162- req .Header .Set ("X-Tenant-Id" , platformTenant )
181+ req .Header .Set (tenantHeader , tenantID )
163182 req .Header .Set ("Content-Type" , "application/json" )
164183
165184 resp , err := c .httpClient .Do (req )
@@ -171,12 +190,12 @@ func (c *backendClient) Notify(ctx context.Context, message string) error {
171190 if resp .StatusCode >= 400 {
172191 body , _ := io .ReadAll (resp .Body )
173192 return catcher .Error ("notify call returned error status" , nil , map [string ]any {
174- "status" : resp .StatusCode , "body" : string (body ),
193+ "status" : resp .StatusCode , "body" : string (body ), "tenantId" : tenantID ,
175194 })
176195 }
177196 return nil
178197}
179198
180- func floodNotificationMessage (ruleName string , threshold int64 , dataSource string ) string {
181- return fmt .Sprintf (notificationMessageTemplate , ruleName , threshold , dataSource )
199+ func floodNotificationMessage (tenantID , ruleName string , count int64 , dataSource string , windowHours int ) string {
200+ return fmt .Sprintf (notificationMessageTemplate , ruleName , count , dataSource , tenantID , windowHours )
182201}
0 commit comments