Skip to content

Commit ccbd16d

Browse files
Copilotnick-benoit
andcommitted
Implement HTTP client and CRUD operations for security detection rules
Co-authored-by: nick-benoit <[email protected]>
1 parent 32d0255 commit ccbd16d

File tree

5 files changed

+622
-15
lines changed

5 files changed

+622
-15
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package detection_rule
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/url"
8+
9+
"github.com/elastic/terraform-provider-elasticstack/internal/clients"
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
)
12+
13+
// SecurityDetectionRuleRequest represents a security detection rule creation/update request
14+
type SecurityDetectionRuleRequest struct {
15+
Name string `json:"name"`
16+
Description string `json:"description"`
17+
Type string `json:"type"`
18+
Query *string `json:"query,omitempty"`
19+
Language *string `json:"language,omitempty"`
20+
Index []string `json:"index,omitempty"`
21+
Severity string `json:"severity"`
22+
Risk int `json:"risk_score"`
23+
Enabled bool `json:"enabled"`
24+
Tags []string `json:"tags,omitempty"`
25+
From string `json:"from"`
26+
To string `json:"to"`
27+
Interval string `json:"interval"`
28+
Meta *map[string]any `json:"meta,omitempty"`
29+
Author []string `json:"author,omitempty"`
30+
License *string `json:"license,omitempty"`
31+
RuleNameOverride *string `json:"rule_name_override,omitempty"`
32+
TimestampOverride *string `json:"timestamp_override,omitempty"`
33+
Note *string `json:"note,omitempty"`
34+
References []string `json:"references,omitempty"`
35+
FalsePositives []string `json:"false_positives,omitempty"`
36+
ExceptionsList []any `json:"exceptions_list,omitempty"`
37+
Version int `json:"version"`
38+
MaxSignals int `json:"max_signals"`
39+
}
40+
41+
// SecurityDetectionRuleResponse represents the API response for a security detection rule
42+
type SecurityDetectionRuleResponse struct {
43+
ID string `json:"id"`
44+
Name string `json:"name"`
45+
Description string `json:"description"`
46+
Type string `json:"type"`
47+
Query *string `json:"query,omitempty"`
48+
Language *string `json:"language,omitempty"`
49+
Index []string `json:"index,omitempty"`
50+
Severity string `json:"severity"`
51+
Risk int `json:"risk_score"`
52+
Enabled bool `json:"enabled"`
53+
Tags []string `json:"tags,omitempty"`
54+
From string `json:"from"`
55+
To string `json:"to"`
56+
Interval string `json:"interval"`
57+
Meta *map[string]any `json:"meta,omitempty"`
58+
Author []string `json:"author,omitempty"`
59+
License *string `json:"license,omitempty"`
60+
RuleNameOverride *string `json:"rule_name_override,omitempty"`
61+
TimestampOverride *string `json:"timestamp_override,omitempty"`
62+
Note *string `json:"note,omitempty"`
63+
References []string `json:"references,omitempty"`
64+
FalsePositives []string `json:"false_positives,omitempty"`
65+
ExceptionsList []any `json:"exceptions_list,omitempty"`
66+
Version int `json:"version"`
67+
MaxSignals int `json:"max_signals"`
68+
CreatedAt string `json:"created_at"`
69+
CreatedBy string `json:"created_by"`
70+
UpdatedAt string `json:"updated_at"`
71+
UpdatedBy string `json:"updated_by"`
72+
}
73+
74+
// CreateSecurityDetectionRule creates a new security detection rule
75+
func CreateSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId string, ruleId *string, rule *SecurityDetectionRuleRequest) (*SecurityDetectionRuleResponse, diag.Diagnostics) {
76+
var diags diag.Diagnostics
77+
78+
kbClient, err := client.GetKibanaClient()
79+
if err != nil {
80+
diags.AddError("Failed to get Kibana client", err.Error())
81+
return nil, diags
82+
}
83+
84+
// Create the URL path
85+
path := fmt.Sprintf("/s/%s/api/detection_engine/rules", url.PathEscape(spaceId))
86+
87+
// Execute the request using resty
88+
resp, err := kbClient.Client.R().SetBody(rule).Post(path)
89+
if err != nil {
90+
diags.AddError("Failed to execute request", err.Error())
91+
return nil, diags
92+
}
93+
94+
// Handle non-2xx status codes
95+
if resp.StatusCode() >= 300 {
96+
diags.AddError(
97+
"API request failed",
98+
fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())),
99+
)
100+
return nil, diags
101+
}
102+
103+
// Parse the response
104+
var result SecurityDetectionRuleResponse
105+
if err := json.Unmarshal(resp.Body(), &result); err != nil {
106+
diags.AddError("Failed to decode response", err.Error())
107+
return nil, diags
108+
}
109+
110+
return &result, diags
111+
}
112+
113+
// GetSecurityDetectionRule retrieves a security detection rule by ID
114+
func GetSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId, ruleId string) (*SecurityDetectionRuleResponse, diag.Diagnostics) {
115+
var diags diag.Diagnostics
116+
117+
kbClient, err := client.GetKibanaClient()
118+
if err != nil {
119+
diags.AddError("Failed to get Kibana client", err.Error())
120+
return nil, diags
121+
}
122+
123+
// Create the URL path
124+
path := fmt.Sprintf("/s/%s/api/detection_engine/rules?id=%s", url.PathEscape(spaceId), url.QueryEscape(ruleId))
125+
126+
// Execute the request using resty
127+
resp, err := kbClient.Client.R().Get(path)
128+
if err != nil {
129+
diags.AddError("Failed to execute request", err.Error())
130+
return nil, diags
131+
}
132+
133+
// Handle not found
134+
if resp.StatusCode() == 404 {
135+
return nil, diags // Rule not found
136+
}
137+
138+
// Handle other non-2xx status codes
139+
if resp.StatusCode() >= 300 {
140+
diags.AddError(
141+
"API request failed",
142+
fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())),
143+
)
144+
return nil, diags
145+
}
146+
147+
// Parse the response
148+
var result SecurityDetectionRuleResponse
149+
if err := json.Unmarshal(resp.Body(), &result); err != nil {
150+
diags.AddError("Failed to decode response", err.Error())
151+
return nil, diags
152+
}
153+
154+
return &result, diags
155+
}
156+
157+
// UpdateSecurityDetectionRule updates an existing security detection rule
158+
func UpdateSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId, ruleId string, rule *SecurityDetectionRuleRequest) (*SecurityDetectionRuleResponse, diag.Diagnostics) {
159+
var diags diag.Diagnostics
160+
161+
kbClient, err := client.GetKibanaClient()
162+
if err != nil {
163+
diags.AddError("Failed to get Kibana client", err.Error())
164+
return nil, diags
165+
}
166+
167+
// Create the URL path
168+
path := fmt.Sprintf("/s/%s/api/detection_engine/rules", url.PathEscape(spaceId))
169+
170+
// Execute the request using resty
171+
resp, err := kbClient.Client.R().SetBody(rule).Put(path)
172+
if err != nil {
173+
diags.AddError("Failed to execute request", err.Error())
174+
return nil, diags
175+
}
176+
177+
// Handle non-2xx status codes
178+
if resp.StatusCode() >= 300 {
179+
diags.AddError(
180+
"API request failed",
181+
fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())),
182+
)
183+
return nil, diags
184+
}
185+
186+
// Parse the response
187+
var result SecurityDetectionRuleResponse
188+
if err := json.Unmarshal(resp.Body(), &result); err != nil {
189+
diags.AddError("Failed to decode response", err.Error())
190+
return nil, diags
191+
}
192+
193+
return &result, diags
194+
}
195+
196+
// DeleteSecurityDetectionRule deletes a security detection rule by ID
197+
func DeleteSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId, ruleId string) diag.Diagnostics {
198+
var diags diag.Diagnostics
199+
200+
kbClient, err := client.GetKibanaClient()
201+
if err != nil {
202+
diags.AddError("Failed to get Kibana client", err.Error())
203+
return diags
204+
}
205+
206+
// Create the URL path
207+
path := fmt.Sprintf("/s/%s/api/detection_engine/rules?id=%s", url.PathEscape(spaceId), url.QueryEscape(ruleId))
208+
209+
// Execute the request using resty
210+
resp, err := kbClient.Client.R().Delete(path)
211+
if err != nil {
212+
diags.AddError("Failed to execute request", err.Error())
213+
return diags
214+
}
215+
216+
// Handle not found (rule might already be deleted)
217+
if resp.StatusCode() == 404 {
218+
return diags // Already deleted, no error
219+
}
220+
221+
// Handle other non-2xx status codes
222+
if resp.StatusCode() >= 300 {
223+
diags.AddError(
224+
"API request failed",
225+
fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())),
226+
)
227+
return diags
228+
}
229+
230+
return diags
231+
}

0 commit comments

Comments
 (0)