-
Couldn't load subscription status.
- Fork 121
Add Support for Kibana Security Detection Rules #1291
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
Changes from 5 commits
0b2e3eb
32d0255
ccbd16d
eb0b1ad
a0d49ba
cc0b67b
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 |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| --- | ||
| # generated by https://github.com/hashicorp/terraform-plugin-docs | ||
| page_title: "elasticstack_kibana_security_detection_rule Resource - terraform-provider-elasticstack" | ||
| subcategory: "" | ||
| description: |- | ||
| Creates or updates a Kibana security detection rule. See https://www.elastic.co/guide/en/security/current/rules-api-create.html | ||
| --- | ||
|
|
||
| # elasticstack_kibana_security_detection_rule (Resource) | ||
|
|
||
| Creates or updates a Kibana security detection rule. See https://www.elastic.co/guide/en/security/current/rules-api-create.html | ||
|
|
||
|
|
||
|
|
||
| <!-- schema generated by tfplugindocs --> | ||
| ## Schema | ||
|
|
||
| ### Required | ||
|
|
||
| - `description` (String) The description of the detection rule. | ||
| - `name` (String) The name of the detection rule. | ||
| - `severity` (String) The severity of the rule. Valid values are: low, medium, high, critical. | ||
| - `type` (String) The rule type. Valid values are: eql, query, machine_learning, threshold, threat_match, new_terms. | ||
|
|
||
| ### Optional | ||
|
|
||
| - `author` (List of String) String array containing the rule's author(s). | ||
| - `enabled` (Boolean) Determines whether the rule is enabled. | ||
| - `exceptions_list` (List of String) List of exceptions that prevent alerts from being generated. | ||
| - `false_positives` (List of String) String array describing common reasons why the rule may issue false-positive alerts. | ||
| - `from` (String) Time from which data is analyzed each time the rule executes, using date math syntax. | ||
| - `index` (List of String) A list of index patterns to search. | ||
| - `interval` (String) How often the rule executes. | ||
| - `kibana_connection` (Block List) Kibana connection configuration block. (see [below for nested schema](#nestedblock--kibana_connection)) | ||
| - `language` (String) The query language. Valid values are: kuery, lucene, eql. | ||
| - `license` (String) The rule's license. | ||
| - `max_signals` (Number) Maximum number of alerts the rule can produce during a single execution. | ||
| - `meta` (String) Optional metadata about the rule as a JSON string. | ||
| - `note` (String) Notes to help investigate alerts produced by the rule. | ||
| - `query` (String) The query that the rule will use to generate alerts. | ||
| - `references` (List of String) String array containing notes about or references to relevant information about the rule. | ||
| - `risk` (Number) A numerical representation of the alert's severity from 1-100. | ||
| - `rule_id` (String) The identifier for the rule. If not provided, an ID is randomly generated. | ||
| - `rule_name_override` (String) Sets the source field for the alert's rule name. | ||
| - `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. | ||
| - `tags` (List of String) String array containing words and phrases to help categorize, filter, and search rules. | ||
| - `timestamp_override` (String) Sets the time field used to query indices. | ||
| - `to` (String) Time to which data is analyzed each time the rule executes, using date math syntax. | ||
| - `version` (Number) The rule's version number. | ||
|
|
||
| ### Read-Only | ||
|
|
||
| - `id` (String) Internal identifier of the resource | ||
|
|
||
| <a id="nestedblock--kibana_connection"></a> | ||
| ### Nested Schema for `kibana_connection` | ||
|
|
||
| Optional: | ||
|
|
||
| - `api_key` (String, Sensitive) API Key to use for authentication to Kibana | ||
| - `ca_certs` (List of String) A list of paths to CA certificates to validate the certificate presented by the Kibana server. | ||
| - `endpoints` (List of String, Sensitive) A comma-separated list of endpoints where the terraform provider will point to, this must include the http(s) schema and port number. | ||
| - `insecure` (Boolean) Disable TLS certificate validation | ||
| - `password` (String, Sensitive) Password to use for API authentication to Kibana. | ||
| - `username` (String) Username to use for API authentication to Kibana. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package detection_rule_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/elastic/terraform-provider-elasticstack/internal/acctest" | ||
| "github.com/hashicorp/terraform-plugin-testing/helper/resource" | ||
| ) | ||
|
|
||
| func TestAccResourceKibanaSecurityDetectionRule(t *testing.T) { | ||
nick-benoit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| resource.Test(t, resource.TestCase{ | ||
| PreCheck: func() { acctest.PreCheck(t) }, | ||
| ProtoV6ProviderFactories: acctest.Providers, | ||
| Steps: []resource.TestStep{ | ||
| { | ||
| Config: testAccResourceKibanaSecurityDetectionRuleCreate(), | ||
| Check: resource.ComposeTestCheckFunc( | ||
| resource.TestCheckResourceAttr("elasticstack_kibana_security_detection_rule.test", "name", "Test Detection Rule"), | ||
| resource.TestCheckResourceAttr("elasticstack_kibana_security_detection_rule.test", "description", "Test security detection rule"), | ||
| resource.TestCheckResourceAttr("elasticstack_kibana_security_detection_rule.test", "type", "query"), | ||
| resource.TestCheckResourceAttr("elasticstack_kibana_security_detection_rule.test", "severity", "medium"), | ||
| resource.TestCheckResourceAttr("elasticstack_kibana_security_detection_rule.test", "enabled", "true"), | ||
| ), | ||
| }, | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| func testAccResourceKibanaSecurityDetectionRuleCreate() string { | ||
| return ` | ||
| provider "elasticstack" { | ||
| kibana {} | ||
| } | ||
|
|
||
| resource "elasticstack_kibana_security_detection_rule" "test" { | ||
| name = "Test Detection Rule" | ||
| description = "Test security detection rule" | ||
| type = "query" | ||
| query = "*:*" | ||
| language = "kuery" | ||
| severity = "medium" | ||
| enabled = true | ||
| tags = ["test"] | ||
| interval = "5m" | ||
| from = "now-6m" | ||
| to = "now" | ||
| }` | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| package detection_rule | ||
nick-benoit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/url" | ||
|
|
||
| "github.com/elastic/terraform-provider-elasticstack/internal/clients" | ||
| "github.com/hashicorp/terraform-plugin-framework/diag" | ||
| ) | ||
|
|
||
| // SecurityDetectionRuleRequest represents a security detection rule creation/update request | ||
| type SecurityDetectionRuleRequest struct { | ||
| Name string `json:"name"` | ||
| Description string `json:"description"` | ||
| Type string `json:"type"` | ||
| Query *string `json:"query,omitempty"` | ||
| Language *string `json:"language,omitempty"` | ||
| Index []string `json:"index,omitempty"` | ||
| Severity string `json:"severity"` | ||
| Risk int `json:"risk_score"` | ||
| Enabled bool `json:"enabled"` | ||
| Tags []string `json:"tags,omitempty"` | ||
| From string `json:"from"` | ||
| To string `json:"to"` | ||
| Interval string `json:"interval"` | ||
| Meta *map[string]any `json:"meta,omitempty"` | ||
| Author []string `json:"author,omitempty"` | ||
| License *string `json:"license,omitempty"` | ||
| RuleNameOverride *string `json:"rule_name_override,omitempty"` | ||
| TimestampOverride *string `json:"timestamp_override,omitempty"` | ||
| Note *string `json:"note,omitempty"` | ||
| References []string `json:"references,omitempty"` | ||
| FalsePositives []string `json:"false_positives,omitempty"` | ||
|
Check failure on line 35 in internal/kibana/security/detection_rule/client.go
|
||
| ExceptionsList []any `json:"exceptions_list,omitempty"` | ||
|
Check failure on line 36 in internal/kibana/security/detection_rule/client.go
|
||
| Version int `json:"version"` | ||
|
Check failure on line 37 in internal/kibana/security/detection_rule/client.go
|
||
| MaxSignals int `json:"max_signals"` | ||
|
Check failure on line 38 in internal/kibana/security/detection_rule/client.go
|
||
| } | ||
|
|
||
| // SecurityDetectionRuleResponse represents the API response for a security detection rule | ||
| type SecurityDetectionRuleResponse struct { | ||
| ID string `json:"id"` | ||
| Name string `json:"name"` | ||
| Description string `json:"description"` | ||
| Type string `json:"type"` | ||
| Query *string `json:"query,omitempty"` | ||
| Language *string `json:"language,omitempty"` | ||
|
Check failure on line 48 in internal/kibana/security/detection_rule/client.go
|
||
| Index []string `json:"index,omitempty"` | ||
| Severity string `json:"severity"` | ||
| Risk int `json:"risk_score"` | ||
| Enabled bool `json:"enabled"` | ||
| Tags []string `json:"tags,omitempty"` | ||
| From string `json:"from"` | ||
| To string `json:"to"` | ||
| Interval string `json:"interval"` | ||
| Meta *map[string]any `json:"meta,omitempty"` | ||
| Author []string `json:"author,omitempty"` | ||
| License *string `json:"license,omitempty"` | ||
| RuleNameOverride *string `json:"rule_name_override,omitempty"` | ||
| TimestampOverride *string `json:"timestamp_override,omitempty"` | ||
| Note *string `json:"note,omitempty"` | ||
| References []string `json:"references,omitempty"` | ||
| FalsePositives []string `json:"false_positives,omitempty"` | ||
| ExceptionsList []any `json:"exceptions_list,omitempty"` | ||
| Version int `json:"version"` | ||
| MaxSignals int `json:"max_signals"` | ||
| CreatedAt string `json:"created_at"` | ||
| CreatedBy string `json:"created_by"` | ||
| UpdatedAt string `json:"updated_at"` | ||
| UpdatedBy string `json:"updated_by"` | ||
| } | ||
|
|
||
| // CreateSecurityDetectionRule creates a new security detection rule | ||
| func CreateSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId string, ruleId *string, rule *SecurityDetectionRuleRequest) (*SecurityDetectionRuleResponse, diag.Diagnostics) { | ||
| var diags diag.Diagnostics | ||
|
|
||
| kbClient, err := client.GetKibanaClient() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| if err != nil { | ||
| diags.AddError("Failed to get Kibana client", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Create the URL path | ||
| path := fmt.Sprintf("/s/%s/api/detection_engine/rules", url.PathEscape(spaceId)) | ||
|
|
||
| // Execute the request using resty | ||
| resp, err := kbClient.Client.R().SetBody(rule).Post(path) | ||
| if err != nil { | ||
| diags.AddError("Failed to execute request", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Handle non-2xx status codes | ||
| if resp.StatusCode() >= 300 { | ||
| diags.AddError( | ||
| "API request failed", | ||
| fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())), | ||
| ) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Parse the response | ||
| var result SecurityDetectionRuleResponse | ||
| if err := json.Unmarshal(resp.Body(), &result); err != nil { | ||
| diags.AddError("Failed to decode response", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| return &result, diags | ||
| } | ||
|
|
||
| // GetSecurityDetectionRule retrieves a security detection rule by ID | ||
| func GetSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId, ruleId string) (*SecurityDetectionRuleResponse, diag.Diagnostics) { | ||
| var diags diag.Diagnostics | ||
|
|
||
| kbClient, err := client.GetKibanaClient() | ||
| if err != nil { | ||
| diags.AddError("Failed to get Kibana client", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Create the URL path | ||
| path := fmt.Sprintf("/s/%s/api/detection_engine/rules?id=%s", url.PathEscape(spaceId), url.QueryEscape(ruleId)) | ||
|
|
||
| // Execute the request using resty | ||
| resp, err := kbClient.Client.R().Get(path) | ||
| if err != nil { | ||
| diags.AddError("Failed to execute request", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Handle not found | ||
| if resp.StatusCode() == 404 { | ||
| return nil, diags // Rule not found | ||
| } | ||
|
|
||
| // Handle other non-2xx status codes | ||
| if resp.StatusCode() >= 300 { | ||
| diags.AddError( | ||
| "API request failed", | ||
| fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())), | ||
| ) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Parse the response | ||
| var result SecurityDetectionRuleResponse | ||
| if err := json.Unmarshal(resp.Body(), &result); err != nil { | ||
| diags.AddError("Failed to decode response", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| return &result, diags | ||
| } | ||
|
|
||
| // UpdateSecurityDetectionRule updates an existing security detection rule | ||
| func UpdateSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId, ruleId string, rule *SecurityDetectionRuleRequest) (*SecurityDetectionRuleResponse, diag.Diagnostics) { | ||
| var diags diag.Diagnostics | ||
|
|
||
| kbClient, err := client.GetKibanaClient() | ||
| if err != nil { | ||
| diags.AddError("Failed to get Kibana client", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Create the URL path | ||
| path := fmt.Sprintf("/s/%s/api/detection_engine/rules", url.PathEscape(spaceId)) | ||
|
|
||
| // Execute the request using resty | ||
| resp, err := kbClient.Client.R().SetBody(rule).Put(path) | ||
| if err != nil { | ||
| diags.AddError("Failed to execute request", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Handle non-2xx status codes | ||
| if resp.StatusCode() >= 300 { | ||
| diags.AddError( | ||
| "API request failed", | ||
| fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())), | ||
| ) | ||
| return nil, diags | ||
| } | ||
|
|
||
| // Parse the response | ||
| var result SecurityDetectionRuleResponse | ||
| if err := json.Unmarshal(resp.Body(), &result); err != nil { | ||
| diags.AddError("Failed to decode response", err.Error()) | ||
| return nil, diags | ||
| } | ||
|
|
||
| return &result, diags | ||
| } | ||
|
|
||
| // DeleteSecurityDetectionRule deletes a security detection rule by ID | ||
| func DeleteSecurityDetectionRule(ctx context.Context, client *clients.ApiClient, spaceId, ruleId string) diag.Diagnostics { | ||
| var diags diag.Diagnostics | ||
|
|
||
| kbClient, err := client.GetKibanaClient() | ||
| if err != nil { | ||
| diags.AddError("Failed to get Kibana client", err.Error()) | ||
| return diags | ||
| } | ||
|
|
||
| // Create the URL path | ||
| path := fmt.Sprintf("/s/%s/api/detection_engine/rules?id=%s", url.PathEscape(spaceId), url.QueryEscape(ruleId)) | ||
|
|
||
| // Execute the request using resty | ||
| resp, err := kbClient.Client.R().Delete(path) | ||
| if err != nil { | ||
| diags.AddError("Failed to execute request", err.Error()) | ||
| return diags | ||
| } | ||
|
|
||
| // Handle not found (rule might already be deleted) | ||
| if resp.StatusCode() == 404 { | ||
| return diags // Already deleted, no error | ||
| } | ||
|
|
||
| // Handle other non-2xx status codes | ||
| if resp.StatusCode() >= 300 { | ||
| diags.AddError( | ||
| "API request failed", | ||
| fmt.Sprintf("Status: %d, URL: %s, Body: %s", resp.StatusCode(), resp.Request.URL, string(resp.Body())), | ||
| ) | ||
|
Check failure on line 226 in internal/kibana/security/detection_rule/client.go
|
||
| return diags | ||
| } | ||
|
|
||
| return diags | ||
| } | ||
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.
Add a terraform usage example