From 25c08c84fb91da768f37f1c1f00c02a9d4c50483 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 09:57:16 -0400 Subject: [PATCH 01/10] feat: Add prebuilt_rules resource --- docs/resources/kibana_prebuilt_rule.md | 55 ++++ .../import.sh | 1 + .../resource.tf | 11 + internal/kibana/prebuilt_rules/acc_test.go | 72 +++++ internal/kibana/prebuilt_rules/create.go | 61 +++++ internal/kibana/prebuilt_rules/delete.go | 40 +++ internal/kibana/prebuilt_rules/models.go | 245 ++++++++++++++++++ internal/kibana/prebuilt_rules/read.go | 39 +++ internal/kibana/prebuilt_rules/resource.go | 33 +++ internal/kibana/prebuilt_rules/schema.go | 70 +++++ internal/kibana/prebuilt_rules/update.go | 67 +++++ provider/plugin_framework.go | 2 + 12 files changed, 696 insertions(+) create mode 100644 docs/resources/kibana_prebuilt_rule.md create mode 100644 examples/resources/elasticstack_kibana_prebuilt_rule/import.sh create mode 100644 examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf create mode 100644 internal/kibana/prebuilt_rules/acc_test.go create mode 100644 internal/kibana/prebuilt_rules/create.go create mode 100644 internal/kibana/prebuilt_rules/delete.go create mode 100644 internal/kibana/prebuilt_rules/models.go create mode 100644 internal/kibana/prebuilt_rules/read.go create mode 100644 internal/kibana/prebuilt_rules/resource.go create mode 100644 internal/kibana/prebuilt_rules/schema.go create mode 100644 internal/kibana/prebuilt_rules/update.go diff --git a/docs/resources/kibana_prebuilt_rule.md b/docs/resources/kibana_prebuilt_rule.md new file mode 100644 index 000000000..ba43c8ac9 --- /dev/null +++ b/docs/resources/kibana_prebuilt_rule.md @@ -0,0 +1,55 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "elasticstack_kibana_prebuilt_rule Resource - terraform-provider-elasticstack" +subcategory: "" +description: |- + Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines, and optionally enables/disables rules based on specified tags. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html +--- + +# elasticstack_kibana_prebuilt_rule (Resource) + +Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines, and optionally enables/disables rules based on specified tags. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html + +## Example Usage + +```terraform +provider "elasticstack" { + kibana {} +} + +resource "elasticstack_kibana_prebuilt_rule" "enable" { + tags = ["OS: Linux", "OS: Windows"] +} + +resource "elasticstack_kibana_prebuilt_rule" "install_no_enable" { + tags = [] +} +``` + + +## Schema + +### Optional + +- `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. +- `tags` (List of String) A list of tag names to filter prebuilt rules for enabling/disabling. Use ['all'] to enable all prebuilt rules, or an empty list to install but not enable any rules. + +### Read-Only + +- `id` (String) The ID of this resource. +- `rules_installed` (Number) Number of prebuilt rules that are installed. +- `rules_not_installed` (Number) Number of prebuilt rules that are not installed. +- `rules_not_updated` (Number) Number of prebuilt rules that have updates available. +- `timelines_installed` (Number) Number of prebuilt timelines that are installed. +- `timelines_not_installed` (Number) Number of prebuilt timelines that are not installed. +- `timelines_not_updated` (Number) Number of prebuilt timelines that have updates available. + +## Import + +Import is supported using the following syntax: + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + +```shell +terraform import elasticstack_kibana_prebuilt_rule.enable "default" +``` diff --git a/examples/resources/elasticstack_kibana_prebuilt_rule/import.sh b/examples/resources/elasticstack_kibana_prebuilt_rule/import.sh new file mode 100644 index 000000000..6ec93ecf7 --- /dev/null +++ b/examples/resources/elasticstack_kibana_prebuilt_rule/import.sh @@ -0,0 +1 @@ +terraform import elasticstack_kibana_prebuilt_rule.enable "default" diff --git a/examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf b/examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf new file mode 100644 index 000000000..724e52bf0 --- /dev/null +++ b/examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf @@ -0,0 +1,11 @@ +provider "elasticstack" { + kibana {} +} + +resource "elasticstack_kibana_prebuilt_rule" "enable" { + tags = ["OS: Linux", "OS: Windows"] +} + +resource "elasticstack_kibana_prebuilt_rule" "install_no_enable" { + tags = [] +} diff --git a/internal/kibana/prebuilt_rules/acc_test.go b/internal/kibana/prebuilt_rules/acc_test.go new file mode 100644 index 000000000..11459d3b0 --- /dev/null +++ b/internal/kibana/prebuilt_rules/acc_test.go @@ -0,0 +1,72 @@ +package prebuilt_rules_test + +import ( + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccResourcePrebuiltRules(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccPrebuiltRuleConfigBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "space_id", "default"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "rules_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "rules_not_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "rules_not_updated"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "timelines_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "timelines_not_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "timelines_not_updated"), + ), + }, + }, + }) +} + +func TestAccResourcePrebuiltRule_withTags(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + CheckDestroy: testAccPrebuiltRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccPrebuiltRuleConfigWithTags(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "space_id", "default"), + resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "tags.#", "2"), + resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "tags.0", "OS: Linux"), + resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "tags.1", "OS: Windows"), + ), + }, + }, + }) +} + +func testAccPrebuiltRuleDestroy(s *terraform.State) error { + // For prebuilt rules, there's nothing to destroy + // The rules remain in Kibana as they are managed by Elastic + return nil +} + +func testAccPrebuiltRuleConfigBasic() string { + return ` +resource "elasticstack_kibana_prebuilt_rule" "test" { + space_id = "default" +} +` +} + +func testAccPrebuiltRuleConfigWithTags() string { + return ` +resource "elasticstack_kibana_prebuilt_rule" "test" { + space_id = "default" + tags = ["OS: Linux", "OS: Windows"] +} +` +} diff --git a/internal/kibana/prebuilt_rules/create.go b/internal/kibana/prebuilt_rules/create.go new file mode 100644 index 000000000..eeed11b51 --- /dev/null +++ b/internal/kibana/prebuilt_rules/create.go @@ -0,0 +1,61 @@ +package prebuilt_rules + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var model prebuiltRuleModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + spaceID := model.SpaceID.ValueString() + + // Install/update prebuilt rules and timelines + resp.Diagnostics.Append(installPrebuiltRules(ctx, client, spaceID)...) + if resp.Diagnostics.HasError() { + return + } + + // Enable/disable rules based on tags if specified + tags, tagDiags := model.getTags(ctx) + resp.Diagnostics.Append(tagDiags...) + if resp.Diagnostics.HasError() { + return + } + + if len(tags) > 0 { + resp.Diagnostics.Append(manageRulesByTags(ctx, client, spaceID, tags)...) + if resp.Diagnostics.HasError() { + return + } + } + + // Set the resource ID to the space ID + model.ID = model.SpaceID + + // Read the current status to populate computed attributes + status, statusDiags := getPrebuiltRulesStatus(ctx, client, spaceID) + resp.Diagnostics.Append(statusDiags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(model.populateFromStatus(ctx, status)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} diff --git a/internal/kibana/prebuilt_rules/delete.go b/internal/kibana/prebuilt_rules/delete.go new file mode 100644 index 000000000..ba74a8c6c --- /dev/null +++ b/internal/kibana/prebuilt_rules/delete.go @@ -0,0 +1,40 @@ +package prebuilt_rules + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *PrebuiltRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var model prebuiltRuleModel + + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + spaceID := model.SpaceID.ValueString() + + // Disable rules that were managed by this resource + tags, tagDiags := model.getTags(ctx) + resp.Diagnostics.Append(tagDiags...) + if resp.Diagnostics.HasError() { + return + } + + if len(tags) > 0 { + resp.Diagnostics.Append(performBulkActionByTags(ctx, client, spaceID, "disable", tags)...) + if resp.Diagnostics.HasError() { + return + } + } + + // The Terraform state will be removed automatically +} diff --git a/internal/kibana/prebuilt_rules/models.go b/internal/kibana/prebuilt_rules/models.go new file mode 100644 index 000000000..f3ecdb6b0 --- /dev/null +++ b/internal/kibana/prebuilt_rules/models.go @@ -0,0 +1,245 @@ +package prebuilt_rules + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type prebuiltRuleModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + Tags types.List `tfsdk:"tags"` // []string + RulesInstalled types.Int64 `tfsdk:"rules_installed"` + RulesNotInstalled types.Int64 `tfsdk:"rules_not_installed"` + RulesNotUpdated types.Int64 `tfsdk:"rules_not_updated"` + TimelinesInstalled types.Int64 `tfsdk:"timelines_installed"` + TimelinesNotInstalled types.Int64 `tfsdk:"timelines_not_installed"` + TimelinesNotUpdated types.Int64 `tfsdk:"timelines_not_updated"` +} + +type prebuiltRulesStatus struct { + RulesInstalled int `json:"rules_installed"` + RulesNotInstalled int `json:"rules_not_installed"` + RulesNotUpdated int `json:"rules_not_updated"` + TimelinesInstalled int `json:"timelines_installed"` + TimelinesNotInstalled int `json:"timelines_not_installed"` + TimelinesNotUpdated int `json:"timelines_not_updated"` +} + +func (model *prebuiltRuleModel) populateFromStatus(ctx context.Context, status *prebuiltRulesStatus) diag.Diagnostics { + if status == nil { + return nil + } + + model.RulesInstalled = types.Int64Value(int64(status.RulesInstalled)) + model.RulesNotInstalled = types.Int64Value(int64(status.RulesNotInstalled)) + model.RulesNotUpdated = types.Int64Value(int64(status.RulesNotUpdated)) + model.TimelinesInstalled = types.Int64Value(int64(status.TimelinesInstalled)) + model.TimelinesNotInstalled = types.Int64Value(int64(status.TimelinesNotInstalled)) + model.TimelinesNotUpdated = types.Int64Value(int64(status.TimelinesNotUpdated)) + + return nil +} + +func (model *prebuiltRuleModel) getTags(ctx context.Context) ([]string, diag.Diagnostics) { + if model.Tags.IsNull() || model.Tags.IsUnknown() { + return nil, nil + } + + var tags []string + diags := model.Tags.ElementsAs(ctx, &tags, false) + return tags, diags +} + +func getPrebuiltRulesStatus(ctx context.Context, client *kibana_oapi.Client, spaceID string) (*prebuiltRulesStatus, diag.Diagnostics) { + var resp *kbapi.ReadPrebuiltRulesAndTimelinesStatusResponse + var err error + + if spaceID != "default" { + resp, err = client.API.ReadPrebuiltRulesAndTimelinesStatusWithResponse(ctx, func(ctx context.Context, req *http.Request) error { + req.Header.Set("kbn-space-id", spaceID) + return nil + }) + } else { + resp, err = client.API.ReadPrebuiltRulesAndTimelinesStatusWithResponse(ctx) + } + + if err != nil { + return nil, utils.FrameworkDiagFromError(err) + } + + if resp.StatusCode() != 200 { + return nil, utils.FrameworkDiagFromError(fmt.Errorf("failed to get prebuilt rules status: %s", resp.Status())) + } + + var status prebuiltRulesStatus + if err := json.Unmarshal(resp.Body, &status); err != nil { + return nil, utils.FrameworkDiagFromError(err) + } + + return &status, nil +} + +func installPrebuiltRules(ctx context.Context, client *kibana_oapi.Client, spaceID string) diag.Diagnostics { + var resp *kbapi.InstallPrebuiltRulesAndTimelinesResponse + var err error + + if spaceID != "default" { + resp, err = client.API.InstallPrebuiltRulesAndTimelinesWithResponse(ctx, func(ctx context.Context, req *http.Request) error { + req.Header.Set("kbn-space-id", spaceID) + return nil + }) + } else { + resp, err = client.API.InstallPrebuiltRulesAndTimelinesWithResponse(ctx) + } + + if err != nil { + return utils.FrameworkDiagFromError(err) + } + + if resp.StatusCode() != 200 { + return utils.FrameworkDiagFromError(fmt.Errorf("failed to install prebuilt rules: %s", resp.Status())) + } + + return nil +} + +func needsRuleUpdate(ctx context.Context, client *kibana_oapi.Client, spaceID string) bool { + status, diags := getPrebuiltRulesStatus(ctx, client, spaceID) + if diags.HasError() { + return true + } + return status.RulesNotInstalled >= 1 || status.RulesNotUpdated >= 1 +} + +func manageRulesByTags(ctx context.Context, client *kibana_oapi.Client, spaceID string, tags []string) diag.Diagnostics { + // Reject "all" as it's not supported by Kibana for large rule sets + if len(tags) == 1 && tags[0] == "all" { + return utils.FrameworkDiagFromError(fmt.Errorf("enabling all rules is not supported due to Kibana API limitations. Please specify specific tags to enable a subset of rules")) + } + + if len(tags) == 0 { + // If no tags specified, this resource manages no rules - this is valid + return nil + } + + // Enable rules matching the specified tags + diags := performBulkActionByTags(ctx, client, spaceID, "enable", tags) + if diags.HasError() { + return diags + } + + return nil +} + +func manageRulesTagTransition(ctx context.Context, client *kibana_oapi.Client, spaceID string, oldTags, newTags []string) diag.Diagnostics { + // Handle tag transitions for declarative behavior + + // Find tags that were removed (need to disable their rules) + var removedTags []string + for _, oldTag := range oldTags { + found := false + for _, newTag := range newTags { + if oldTag == newTag { + found = true + break + } + } + if !found { + removedTags = append(removedTags, oldTag) + } + } + + // Find tags that were added (need to enable their rules) + var addedTags []string + for _, newTag := range newTags { + found := false + for _, oldTag := range oldTags { + if newTag == oldTag { + found = true + break + } + } + if !found { + addedTags = append(addedTags, newTag) + } + } + + // Disable rules for removed tags + if len(removedTags) > 0 { + diags := performBulkActionByTags(ctx, client, spaceID, "disable", removedTags) + if diags.HasError() { + return diags + } + } + + // Enable rules for added tags + if len(addedTags) > 0 { + diags := performBulkActionByTags(ctx, client, spaceID, "enable", addedTags) + if diags.HasError() { + return diags + } + } + + return nil +} + +func performBulkActionByTags(ctx context.Context, client *kibana_oapi.Client, spaceID, action string, tags []string) diag.Diagnostics { + if len(tags) == 0 { + return nil + } + + // Reject "all" as it's not supported by Kibana for large rule sets + if len(tags) == 1 && tags[0] == "all" { + return utils.FrameworkDiagFromError(fmt.Errorf("enabling all rules is not supported due to Kibana API limitations. Please specify specific tags to enable a subset of rules")) + } + + // Build KQL query for specific tags + tagQueries := make([]string, len(tags)) + for i, tag := range tags { + tagQueries[i] = fmt.Sprintf("alert.attributes.tags:\"%s\"", tag) + } + query := strings.Join(tagQueries, " OR ") + + bulkActionBody := map[string]interface{}{ + "action": action, + "query": query, + } + + bodyJSON, err := json.Marshal(bulkActionBody) + if err != nil { + return utils.FrameworkDiagFromError(err) + } + + var resp *kbapi.PerformRulesBulkActionResponse + + if spaceID != "default" { + resp, err = client.API.PerformRulesBulkActionWithBodyWithResponse(ctx, &kbapi.PerformRulesBulkActionParams{}, "application/json", bytes.NewReader(bodyJSON), func(ctx context.Context, req *http.Request) error { + req.Header.Set("kbn-space-id", spaceID) + return nil + }) + } else { + resp, err = client.API.PerformRulesBulkActionWithBodyWithResponse(ctx, &kbapi.PerformRulesBulkActionParams{}, "application/json", bytes.NewReader(bodyJSON)) + } + + if err != nil { + return utils.FrameworkDiagFromError(err) + } + + if resp.StatusCode() != 200 { + bodyStr := string(resp.Body) + return utils.FrameworkDiagFromError(fmt.Errorf("failed to perform bulk %s action on rules: %s. Response: %s", action, resp.Status(), bodyStr)) + } + + return nil +} diff --git a/internal/kibana/prebuilt_rules/read.go b/internal/kibana/prebuilt_rules/read.go new file mode 100644 index 000000000..681de21c0 --- /dev/null +++ b/internal/kibana/prebuilt_rules/read.go @@ -0,0 +1,39 @@ +package prebuilt_rules + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *PrebuiltRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var model prebuiltRuleModel + + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + spaceID := model.ID.ValueString() + + // Get current status + status, statusDiags := getPrebuiltRulesStatus(ctx, client, spaceID) + resp.Diagnostics.Append(statusDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Update computed values from status + resp.Diagnostics.Append(model.populateFromStatus(ctx, status)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} diff --git a/internal/kibana/prebuilt_rules/resource.go b/internal/kibana/prebuilt_rules/resource.go new file mode 100644 index 000000000..7d84b6c77 --- /dev/null +++ b/internal/kibana/prebuilt_rules/resource.go @@ -0,0 +1,33 @@ +package prebuilt_rules + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var ( + _ resource.Resource = &PrebuiltRuleResource{} + _ resource.ResourceWithConfigure = &PrebuiltRuleResource{} +) + +// NewResource is a helper function to simplify the provider implementation. +func NewResource() resource.Resource { + return &PrebuiltRuleResource{} +} + +type PrebuiltRuleResource struct { + client *clients.ApiClient +} + +func (r *PrebuiltRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *PrebuiltRuleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_prebuilt_rule") +} diff --git a/internal/kibana/prebuilt_rules/schema.go b/internal/kibana/prebuilt_rules/schema.go new file mode 100644 index 000000000..59c658aec --- /dev/null +++ b/internal/kibana/prebuilt_rules/schema.go @@ -0,0 +1,70 @@ +package prebuilt_rules + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *PrebuiltRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines, and optionally enables/disables rules based on specified tags. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The ID of this resource.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "space_id": schema.StringAttribute{ + Description: "An identifier for the space. If space_id is not provided, the default space is used.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("default"), + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "tags": schema.ListAttribute{ + Description: "A list of tag names to filter prebuilt rules for enabling/disabling. Use ['all'] to enable all prebuilt rules, or an empty list to install but not enable any rules.", + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(0), + }, + }, + "rules_installed": schema.Int64Attribute{ + Description: "Number of prebuilt rules that are installed.", + Computed: true, + }, + "rules_not_installed": schema.Int64Attribute{ + Description: "Number of prebuilt rules that are not installed.", + Computed: true, + }, + "rules_not_updated": schema.Int64Attribute{ + Description: "Number of prebuilt rules that have updates available.", + Computed: true, + }, + "timelines_installed": schema.Int64Attribute{ + Description: "Number of prebuilt timelines that are installed.", + Computed: true, + }, + "timelines_not_installed": schema.Int64Attribute{ + Description: "Number of prebuilt timelines that are not installed.", + Computed: true, + }, + "timelines_not_updated": schema.Int64Attribute{ + Description: "Number of prebuilt timelines that have updates available.", + Computed: true, + }, + }, + } +} diff --git a/internal/kibana/prebuilt_rules/update.go b/internal/kibana/prebuilt_rules/update.go new file mode 100644 index 000000000..1bb154088 --- /dev/null +++ b/internal/kibana/prebuilt_rules/update.go @@ -0,0 +1,67 @@ +package prebuilt_rules + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *PrebuiltRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model prebuiltRuleModel + var priorModel prebuiltRuleModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + resp.Diagnostics.Append(req.State.Get(ctx, &priorModel)...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError(err.Error(), "") + return + } + + spaceID := model.SpaceID.ValueString() + + // Check if we need to install/update rules + if needsRuleUpdate(ctx, client, spaceID) { + resp.Diagnostics.Append(installPrebuiltRules(ctx, client, spaceID)...) + if resp.Diagnostics.HasError() { + return + } + } + + // Handle tag transitions for declarative behavior + newTags, newTagDiags := model.getTags(ctx) + resp.Diagnostics.Append(newTagDiags...) + if resp.Diagnostics.HasError() { + return + } + + oldTags, oldTagDiags := priorModel.getTags(ctx) + resp.Diagnostics.Append(oldTagDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Use transition logic to handle tag changes declaratively + resp.Diagnostics.Append(manageRulesTagTransition(ctx, client, spaceID, oldTags, newTags)...) + if resp.Diagnostics.HasError() { + return + } + + // Read the current status to populate computed attributes + status, statusDiags := getPrebuiltRulesStatus(ctx, client, spaceID) + resp.Diagnostics.Append(statusDiags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(model.populateFromStatus(ctx, status)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index 4da1e743d..7898d6724 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -23,6 +23,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/kibana/data_view" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/import_saved_objects" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/maintenance_window" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/prebuilt_rules" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/spaces" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/parameter" @@ -114,5 +115,6 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { maintenance_window.NewResource, enrich.NewEnrichPolicyResource, role_mapping.NewRoleMappingResource, + prebuilt_rules.NewResource, } } From a2e726ef25b3740b3340ed049d19eba12f80df67 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 11:15:20 -0400 Subject: [PATCH 02/10] fix: ensure minimum versions is satisfied --- internal/kibana/prebuilt_rules/create.go | 18 ++++++++++++++++ internal/kibana/prebuilt_rules/delete.go | 18 ++++++++++++++++ internal/kibana/prebuilt_rules/read.go | 18 ++++++++++++++++ internal/kibana/prebuilt_rules/update.go | 18 ++++++++++++++++ .../kibana/prebuilt_rules/version_utils.go | 21 +++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 internal/kibana/prebuilt_rules/version_utils.go diff --git a/internal/kibana/prebuilt_rules/create.go b/internal/kibana/prebuilt_rules/create.go index eeed11b51..33980efe0 100644 --- a/internal/kibana/prebuilt_rules/create.go +++ b/internal/kibana/prebuilt_rules/create.go @@ -14,6 +14,24 @@ func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRe return } + serverVersion, diags := r.client.ServerVersion(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + serverFlavor, diags := r.client.ServerFlavor(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/prebuilt_rules/delete.go b/internal/kibana/prebuilt_rules/delete.go index ba74a8c6c..ac035e74e 100644 --- a/internal/kibana/prebuilt_rules/delete.go +++ b/internal/kibana/prebuilt_rules/delete.go @@ -14,6 +14,24 @@ func (r *PrebuiltRuleResource) Delete(ctx context.Context, req resource.DeleteRe return } + serverVersion, diags := r.client.ServerVersion(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + serverFlavor, diags := r.client.ServerFlavor(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/prebuilt_rules/read.go b/internal/kibana/prebuilt_rules/read.go index 681de21c0..a79afbca8 100644 --- a/internal/kibana/prebuilt_rules/read.go +++ b/internal/kibana/prebuilt_rules/read.go @@ -14,6 +14,24 @@ func (r *PrebuiltRuleResource) Read(ctx context.Context, req resource.ReadReques return } + serverVersion, diags := r.client.ServerVersion(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + serverFlavor, diags := r.client.ServerFlavor(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/prebuilt_rules/update.go b/internal/kibana/prebuilt_rules/update.go index 1bb154088..0fe34cc85 100644 --- a/internal/kibana/prebuilt_rules/update.go +++ b/internal/kibana/prebuilt_rules/update.go @@ -16,6 +16,24 @@ func (r *PrebuiltRuleResource) Update(ctx context.Context, req resource.UpdateRe return } + serverVersion, diags := r.client.ServerVersion(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + serverFlavor, diags := r.client.ServerFlavor(ctx) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + client, err := r.client.GetKibanaOapiClient() if err != nil { resp.Diagnostics.AddError(err.Error(), "") diff --git a/internal/kibana/prebuilt_rules/version_utils.go b/internal/kibana/prebuilt_rules/version_utils.go new file mode 100644 index 000000000..4226ce0c3 --- /dev/null +++ b/internal/kibana/prebuilt_rules/version_utils.go @@ -0,0 +1,21 @@ +package prebuilt_rules + +import ( + "fmt" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +func validatePrebuiltRulesServer(serverVersion *version.Version, serverFlavor string) diag.Diagnostics { + var serverlessFlavor = "serverless" + var prebuiltRulesMinSupportedVersion = version.Must(version.NewVersion("8.0.0")) + var diags diag.Diagnostics + + if serverVersion.LessThan(prebuiltRulesMinSupportedVersion) && serverFlavor != serverlessFlavor { + diags.AddError("Prebuilt rules API not supported", fmt.Sprintf(`The prebuilt rules feature requires a minimum Elasticsearch version of "%s" or a serverless Kibana instance.`, prebuiltRulesMinSupportedVersion)) + return diags + } + + return nil +} \ No newline at end of file From 8e4e9457329e2abbb871f764b3506e7911eb5642 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 12:50:47 -0400 Subject: [PATCH 03/10] fix diags --- internal/kibana/prebuilt_rules/create.go | 12 +++++------- internal/kibana/prebuilt_rules/delete.go | 12 +++++------- internal/kibana/prebuilt_rules/read.go | 12 +++++------- internal/kibana/prebuilt_rules/update.go | 12 +++++------- internal/kibana/prebuilt_rules/version_utils.go | 2 +- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/internal/kibana/prebuilt_rules/create.go b/internal/kibana/prebuilt_rules/create.go index 33980efe0..8299033f4 100644 --- a/internal/kibana/prebuilt_rules/create.go +++ b/internal/kibana/prebuilt_rules/create.go @@ -14,19 +14,17 @@ func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRe return } - serverVersion, diags := r.client.ServerVersion(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { return } - serverFlavor, diags := r.client.ServerFlavor(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { return } - diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) if diags.HasError() { resp.Diagnostics.Append(diags...) return diff --git a/internal/kibana/prebuilt_rules/delete.go b/internal/kibana/prebuilt_rules/delete.go index ac035e74e..e8884fa62 100644 --- a/internal/kibana/prebuilt_rules/delete.go +++ b/internal/kibana/prebuilt_rules/delete.go @@ -14,19 +14,17 @@ func (r *PrebuiltRuleResource) Delete(ctx context.Context, req resource.DeleteRe return } - serverVersion, diags := r.client.ServerVersion(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { return } - serverFlavor, diags := r.client.ServerFlavor(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { return } - diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) if diags.HasError() { resp.Diagnostics.Append(diags...) return diff --git a/internal/kibana/prebuilt_rules/read.go b/internal/kibana/prebuilt_rules/read.go index a79afbca8..c6a5e829b 100644 --- a/internal/kibana/prebuilt_rules/read.go +++ b/internal/kibana/prebuilt_rules/read.go @@ -14,19 +14,17 @@ func (r *PrebuiltRuleResource) Read(ctx context.Context, req resource.ReadReques return } - serverVersion, diags := r.client.ServerVersion(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { return } - serverFlavor, diags := r.client.ServerFlavor(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { return } - diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) if diags.HasError() { resp.Diagnostics.Append(diags...) return diff --git a/internal/kibana/prebuilt_rules/update.go b/internal/kibana/prebuilt_rules/update.go index 0fe34cc85..b5c9c58c3 100644 --- a/internal/kibana/prebuilt_rules/update.go +++ b/internal/kibana/prebuilt_rules/update.go @@ -16,19 +16,17 @@ func (r *PrebuiltRuleResource) Update(ctx context.Context, req resource.UpdateRe return } - serverVersion, diags := r.client.ServerVersion(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) + if sdkDiags.HasError() { return } - serverFlavor, diags := r.client.ServerFlavor(ctx) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) + if sdkDiags.HasError() { return } - diags = validatePrebuiltRulesServer(serverVersion, serverFlavor) + diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) if diags.HasError() { resp.Diagnostics.Append(diags...) return diff --git a/internal/kibana/prebuilt_rules/version_utils.go b/internal/kibana/prebuilt_rules/version_utils.go index 4226ce0c3..3afe6a280 100644 --- a/internal/kibana/prebuilt_rules/version_utils.go +++ b/internal/kibana/prebuilt_rules/version_utils.go @@ -18,4 +18,4 @@ func validatePrebuiltRulesServer(serverVersion *version.Version, serverFlavor st } return nil -} \ No newline at end of file +} From 1f12585a37a56e7ecf5324ece5ee0e8cd755d7ea Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 13:10:36 -0400 Subject: [PATCH 04/10] fix versions --- internal/kibana/prebuilt_rules/create.go | 17 +++++++-------- internal/kibana/prebuilt_rules/delete.go | 17 +++++++-------- internal/kibana/prebuilt_rules/read.go | 17 +++++++-------- internal/kibana/prebuilt_rules/update.go | 17 +++++++-------- .../kibana/prebuilt_rules/version_utils.go | 21 ------------------- 5 files changed, 28 insertions(+), 61 deletions(-) delete mode 100644 internal/kibana/prebuilt_rules/version_utils.go diff --git a/internal/kibana/prebuilt_rules/create.go b/internal/kibana/prebuilt_rules/create.go index 8299033f4..46ea304c1 100644 --- a/internal/kibana/prebuilt_rules/create.go +++ b/internal/kibana/prebuilt_rules/create.go @@ -3,6 +3,8 @@ package prebuilt_rules import ( "context" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -14,19 +16,14 @@ func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRe return } - serverVersion, sdkDiags := r.client.ServerVersion(ctx) - if sdkDiags.HasError() { - return - } - - serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) - if sdkDiags.HasError() { + isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) + if resp.Diagnostics.HasError() { return } - diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + if !isSupported { + resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } diff --git a/internal/kibana/prebuilt_rules/delete.go b/internal/kibana/prebuilt_rules/delete.go index e8884fa62..8ed526bef 100644 --- a/internal/kibana/prebuilt_rules/delete.go +++ b/internal/kibana/prebuilt_rules/delete.go @@ -3,6 +3,8 @@ package prebuilt_rules import ( "context" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -14,19 +16,14 @@ func (r *PrebuiltRuleResource) Delete(ctx context.Context, req resource.DeleteRe return } - serverVersion, sdkDiags := r.client.ServerVersion(ctx) - if sdkDiags.HasError() { - return - } - - serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) - if sdkDiags.HasError() { + isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) + if resp.Diagnostics.HasError() { return } - diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + if !isSupported { + resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } diff --git a/internal/kibana/prebuilt_rules/read.go b/internal/kibana/prebuilt_rules/read.go index c6a5e829b..56aa71f1f 100644 --- a/internal/kibana/prebuilt_rules/read.go +++ b/internal/kibana/prebuilt_rules/read.go @@ -3,6 +3,8 @@ package prebuilt_rules import ( "context" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -14,19 +16,14 @@ func (r *PrebuiltRuleResource) Read(ctx context.Context, req resource.ReadReques return } - serverVersion, sdkDiags := r.client.ServerVersion(ctx) - if sdkDiags.HasError() { - return - } - - serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) - if sdkDiags.HasError() { + isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) + if resp.Diagnostics.HasError() { return } - diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + if !isSupported { + resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } diff --git a/internal/kibana/prebuilt_rules/update.go b/internal/kibana/prebuilt_rules/update.go index b5c9c58c3..6c07d2dcd 100644 --- a/internal/kibana/prebuilt_rules/update.go +++ b/internal/kibana/prebuilt_rules/update.go @@ -3,6 +3,8 @@ package prebuilt_rules import ( "context" + "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -16,19 +18,14 @@ func (r *PrebuiltRuleResource) Update(ctx context.Context, req resource.UpdateRe return } - serverVersion, sdkDiags := r.client.ServerVersion(ctx) - if sdkDiags.HasError() { - return - } - - serverFlavor, sdkDiags := r.client.ServerFlavor(ctx) - if sdkDiags.HasError() { + isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) + if resp.Diagnostics.HasError() { return } - diags := validatePrebuiltRulesServer(serverVersion, serverFlavor) - if diags.HasError() { - resp.Diagnostics.Append(diags...) + if !isSupported { + resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } diff --git a/internal/kibana/prebuilt_rules/version_utils.go b/internal/kibana/prebuilt_rules/version_utils.go deleted file mode 100644 index 3afe6a280..000000000 --- a/internal/kibana/prebuilt_rules/version_utils.go +++ /dev/null @@ -1,21 +0,0 @@ -package prebuilt_rules - -import ( - "fmt" - - "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-plugin-framework/diag" -) - -func validatePrebuiltRulesServer(serverVersion *version.Version, serverFlavor string) diag.Diagnostics { - var serverlessFlavor = "serverless" - var prebuiltRulesMinSupportedVersion = version.Must(version.NewVersion("8.0.0")) - var diags diag.Diagnostics - - if serverVersion.LessThan(prebuiltRulesMinSupportedVersion) && serverFlavor != serverlessFlavor { - diags.AddError("Prebuilt rules API not supported", fmt.Sprintf(`The prebuilt rules feature requires a minimum Elasticsearch version of "%s" or a serverless Kibana instance.`, prebuiltRulesMinSupportedVersion)) - return diags - } - - return nil -} From 1c98f3292e2e237fc62a307673770a39d3f19651 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 13:28:41 -0400 Subject: [PATCH 05/10] fix verions --- internal/kibana/prebuilt_rules/create.go | 5 +++-- internal/kibana/prebuilt_rules/delete.go | 5 +++-- internal/kibana/prebuilt_rules/read.go | 5 +++-- internal/kibana/prebuilt_rules/update.go | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/kibana/prebuilt_rules/create.go b/internal/kibana/prebuilt_rules/create.go index 46ea304c1..6d02c525d 100644 --- a/internal/kibana/prebuilt_rules/create.go +++ b/internal/kibana/prebuilt_rules/create.go @@ -16,13 +16,14 @@ func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRe return } - isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) if resp.Diagnostics.HasError() { return } - if !isSupported { + minVersion := version.Must(version.NewVersion("8.0.0")) + if serverVersion.LessThan(minVersion) { resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } diff --git a/internal/kibana/prebuilt_rules/delete.go b/internal/kibana/prebuilt_rules/delete.go index 8ed526bef..861f2b730 100644 --- a/internal/kibana/prebuilt_rules/delete.go +++ b/internal/kibana/prebuilt_rules/delete.go @@ -16,13 +16,14 @@ func (r *PrebuiltRuleResource) Delete(ctx context.Context, req resource.DeleteRe return } - isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) if resp.Diagnostics.HasError() { return } - if !isSupported { + minVersion := version.Must(version.NewVersion("8.0.0")) + if serverVersion.LessThan(minVersion) { resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } diff --git a/internal/kibana/prebuilt_rules/read.go b/internal/kibana/prebuilt_rules/read.go index 56aa71f1f..2e306d880 100644 --- a/internal/kibana/prebuilt_rules/read.go +++ b/internal/kibana/prebuilt_rules/read.go @@ -16,13 +16,14 @@ func (r *PrebuiltRuleResource) Read(ctx context.Context, req resource.ReadReques return } - isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) if resp.Diagnostics.HasError() { return } - if !isSupported { + minVersion := version.Must(version.NewVersion("8.0.0")) + if serverVersion.LessThan(minVersion) { resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } diff --git a/internal/kibana/prebuilt_rules/update.go b/internal/kibana/prebuilt_rules/update.go index 6c07d2dcd..09983af07 100644 --- a/internal/kibana/prebuilt_rules/update.go +++ b/internal/kibana/prebuilt_rules/update.go @@ -18,13 +18,14 @@ func (r *PrebuiltRuleResource) Update(ctx context.Context, req resource.UpdateRe return } - isSupported, sdkDiags := r.client.EnforceMinVersion(ctx, version.Must(version.NewVersion("8.0.0"))) + serverVersion, sdkDiags := r.client.ServerVersion(ctx) resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) if resp.Diagnostics.HasError() { return } - if !isSupported { + minVersion := version.Must(version.NewVersion("8.0.0")) + if serverVersion.LessThan(minVersion) { resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") return } From c59ae6065b554b89064cf8e6e1f228be7350e075 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 13:46:34 -0400 Subject: [PATCH 06/10] skip versions not 8.x+ --- internal/kibana/prebuilt_rules/acc_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/kibana/prebuilt_rules/acc_test.go b/internal/kibana/prebuilt_rules/acc_test.go index 11459d3b0..a0485a012 100644 --- a/internal/kibana/prebuilt_rules/acc_test.go +++ b/internal/kibana/prebuilt_rules/acc_test.go @@ -4,17 +4,22 @@ import ( "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) +var minVersionPrebuiltRules = version.Must(version.NewVersion("8.0.0")) + func TestAccResourcePrebuiltRules(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, ProtoV6ProviderFactories: acctest.Providers, Steps: []resource.TestStep{ { - Config: testAccPrebuiltRuleConfigBasic(), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules), + Config: testAccPrebuiltRuleConfigBasic(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "space_id", "default"), resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "rules_installed"), @@ -36,7 +41,8 @@ func TestAccResourcePrebuiltRule_withTags(t *testing.T) { CheckDestroy: testAccPrebuiltRuleDestroy, Steps: []resource.TestStep{ { - Config: testAccPrebuiltRuleConfigWithTags(), + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules), + Config: testAccPrebuiltRuleConfigWithTags(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "space_id", "default"), resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "tags.#", "2"), From 2f579a877fda9c40c93034def47288a22584bbb2 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 14:46:54 -0400 Subject: [PATCH 07/10] fix docs with new generation --- docs/resources/kibana_prebuilt_rule.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/kibana_prebuilt_rule.md b/docs/resources/kibana_prebuilt_rule.md index ba43c8ac9..8fc8ea837 100644 --- a/docs/resources/kibana_prebuilt_rule.md +++ b/docs/resources/kibana_prebuilt_rule.md @@ -1,6 +1,6 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "elasticstack_kibana_prebuilt_rule Resource - terraform-provider-elasticstack" +page_title: "elasticstack_kibana_prebuilt_rule Resource - elasticstack" subcategory: "" description: |- Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines, and optionally enables/disables rules based on specified tags. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html From fad6305e2bb4642a6d1dd51dd954ab7fd316255e Mon Sep 17 00:00:00 2001 From: tehbooom Date: Wed, 10 Sep 2025 14:58:49 -0400 Subject: [PATCH 08/10] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc8a1b402..7d3b3c6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased] +- Create `elasticstack_kibana_prebuilt_rule` resource ([#1296](https://github.com/elastic/terraform-provider-elasticstack/pull/1296)) - Create `elasticstack_kibana_maintenance_window` resource. ([#1224](https://github.com/elastic/terraform-provider-elasticstack/pull/1224)) - Add support for `solution` field in `elasticstack_kibana_space` resource and data source ([#1102](https://github.com/elastic/terraform-provider-elasticstack/issues/1102)) - Add `slo_id` validation to `elasticstack_kibana_slo` ([#1221](https://github.com/elastic/terraform-provider-elasticstack/pull/1221)) From 13eea2b285b2cffeabe13770171afd40a832e96c Mon Sep 17 00:00:00 2001 From: tehbooom Date: Thu, 11 Sep 2025 11:02:02 -0400 Subject: [PATCH 09/10] fix: Only install prebuilt rules --- .../kibana_install_prebuilt_rules.md | 41 ++++ docs/resources/kibana_prebuilt_rule.md | 55 ----- .../resource.tf | 8 + .../import.sh | 1 - .../resource.tf | 11 - internal/kibana/prebuilt_rules/acc_test.go | 38 +--- internal/kibana/prebuilt_rules/create.go | 19 +- internal/kibana/prebuilt_rules/delete.go | 46 +--- internal/kibana/prebuilt_rules/models.go | 208 ++---------------- internal/kibana/prebuilt_rules/read.go | 11 +- internal/kibana/prebuilt_rules/resource.go | 2 +- internal/kibana/prebuilt_rules/schema.go | 13 +- internal/kibana/prebuilt_rules/update.go | 26 +-- 13 files changed, 84 insertions(+), 395 deletions(-) create mode 100644 docs/resources/kibana_install_prebuilt_rules.md delete mode 100644 docs/resources/kibana_prebuilt_rule.md create mode 100644 examples/resources/elasticstack_kibana_install_prebuilt_rules/resource.tf delete mode 100644 examples/resources/elasticstack_kibana_prebuilt_rule/import.sh delete mode 100644 examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf diff --git a/docs/resources/kibana_install_prebuilt_rules.md b/docs/resources/kibana_install_prebuilt_rules.md new file mode 100644 index 000000000..8cf3461c2 --- /dev/null +++ b/docs/resources/kibana_install_prebuilt_rules.md @@ -0,0 +1,41 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "elasticstack_kibana_install_prebuilt_rules Resource - elasticstack" +subcategory: "" +description: |- + Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html +--- + +# elasticstack_kibana_install_prebuilt_rules (Resource) + +Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html + +## Example Usage + +```terraform +provider "elasticstack" { + kibana {} +} + + +resource "elasticstack_kibana_install_prebuilt_rules" "example" { + space_id = "default" +} +``` + + +## Schema + +### Optional + +- `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. + +### Read-Only + +- `id` (String) The ID of this resource. +- `rules_installed` (Number) Number of prebuilt rules that are installed. +- `rules_not_installed` (Number) Number of prebuilt rules that are not installed. +- `rules_not_updated` (Number) Number of prebuilt rules that have updates available. +- `timelines_installed` (Number) Number of prebuilt timelines that are installed. +- `timelines_not_installed` (Number) Number of prebuilt timelines that are not installed. +- `timelines_not_updated` (Number) Number of prebuilt timelines that have updates available. diff --git a/docs/resources/kibana_prebuilt_rule.md b/docs/resources/kibana_prebuilt_rule.md deleted file mode 100644 index 8fc8ea837..000000000 --- a/docs/resources/kibana_prebuilt_rule.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "elasticstack_kibana_prebuilt_rule Resource - elasticstack" -subcategory: "" -description: |- - Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines, and optionally enables/disables rules based on specified tags. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html ---- - -# elasticstack_kibana_prebuilt_rule (Resource) - -Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines, and optionally enables/disables rules based on specified tags. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html - -## Example Usage - -```terraform -provider "elasticstack" { - kibana {} -} - -resource "elasticstack_kibana_prebuilt_rule" "enable" { - tags = ["OS: Linux", "OS: Windows"] -} - -resource "elasticstack_kibana_prebuilt_rule" "install_no_enable" { - tags = [] -} -``` - - -## Schema - -### Optional - -- `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. -- `tags` (List of String) A list of tag names to filter prebuilt rules for enabling/disabling. Use ['all'] to enable all prebuilt rules, or an empty list to install but not enable any rules. - -### Read-Only - -- `id` (String) The ID of this resource. -- `rules_installed` (Number) Number of prebuilt rules that are installed. -- `rules_not_installed` (Number) Number of prebuilt rules that are not installed. -- `rules_not_updated` (Number) Number of prebuilt rules that have updates available. -- `timelines_installed` (Number) Number of prebuilt timelines that are installed. -- `timelines_not_installed` (Number) Number of prebuilt timelines that are not installed. -- `timelines_not_updated` (Number) Number of prebuilt timelines that have updates available. - -## Import - -Import is supported using the following syntax: - -The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: - -```shell -terraform import elasticstack_kibana_prebuilt_rule.enable "default" -``` diff --git a/examples/resources/elasticstack_kibana_install_prebuilt_rules/resource.tf b/examples/resources/elasticstack_kibana_install_prebuilt_rules/resource.tf new file mode 100644 index 000000000..f9971fa5c --- /dev/null +++ b/examples/resources/elasticstack_kibana_install_prebuilt_rules/resource.tf @@ -0,0 +1,8 @@ +provider "elasticstack" { + kibana {} +} + + +resource "elasticstack_kibana_install_prebuilt_rules" "example" { + space_id = "default" +} diff --git a/examples/resources/elasticstack_kibana_prebuilt_rule/import.sh b/examples/resources/elasticstack_kibana_prebuilt_rule/import.sh deleted file mode 100644 index 6ec93ecf7..000000000 --- a/examples/resources/elasticstack_kibana_prebuilt_rule/import.sh +++ /dev/null @@ -1 +0,0 @@ -terraform import elasticstack_kibana_prebuilt_rule.enable "default" diff --git a/examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf b/examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf deleted file mode 100644 index 724e52bf0..000000000 --- a/examples/resources/elasticstack_kibana_prebuilt_rule/resource.tf +++ /dev/null @@ -1,11 +0,0 @@ -provider "elasticstack" { - kibana {} -} - -resource "elasticstack_kibana_prebuilt_rule" "enable" { - tags = ["OS: Linux", "OS: Windows"] -} - -resource "elasticstack_kibana_prebuilt_rule" "install_no_enable" { - tags = [] -} diff --git a/internal/kibana/prebuilt_rules/acc_test.go b/internal/kibana/prebuilt_rules/acc_test.go index a0485a012..3479e5dc9 100644 --- a/internal/kibana/prebuilt_rules/acc_test.go +++ b/internal/kibana/prebuilt_rules/acc_test.go @@ -7,7 +7,6 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" ) var minVersionPrebuiltRules = version.Must(version.NewVersion("8.0.0")) @@ -34,45 +33,10 @@ func TestAccResourcePrebuiltRules(t *testing.T) { }) } -func TestAccResourcePrebuiltRule_withTags(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.Providers, - CheckDestroy: testAccPrebuiltRuleDestroy, - Steps: []resource.TestStep{ - { - SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules), - Config: testAccPrebuiltRuleConfigWithTags(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "space_id", "default"), - resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "tags.#", "2"), - resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "tags.0", "OS: Linux"), - resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "tags.1", "OS: Windows"), - ), - }, - }, - }) -} - -func testAccPrebuiltRuleDestroy(s *terraform.State) error { - // For prebuilt rules, there's nothing to destroy - // The rules remain in Kibana as they are managed by Elastic - return nil -} - func testAccPrebuiltRuleConfigBasic() string { return ` -resource "elasticstack_kibana_prebuilt_rule" "test" { - space_id = "default" -} -` -} - -func testAccPrebuiltRuleConfigWithTags() string { - return ` -resource "elasticstack_kibana_prebuilt_rule" "test" { +resource "elasticstack_kibana_install_prebuilt_rules" "test" { space_id = "default" - tags = ["OS: Linux", "OS: Windows"] } ` } diff --git a/internal/kibana/prebuilt_rules/create.go b/internal/kibana/prebuilt_rules/create.go index 6d02c525d..ffed50423 100644 --- a/internal/kibana/prebuilt_rules/create.go +++ b/internal/kibana/prebuilt_rules/create.go @@ -42,20 +42,6 @@ func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRe return } - // Enable/disable rules based on tags if specified - tags, tagDiags := model.getTags(ctx) - resp.Diagnostics.Append(tagDiags...) - if resp.Diagnostics.HasError() { - return - } - - if len(tags) > 0 { - resp.Diagnostics.Append(manageRulesByTags(ctx, client, spaceID, tags)...) - if resp.Diagnostics.HasError() { - return - } - } - // Set the resource ID to the space ID model.ID = model.SpaceID @@ -66,10 +52,7 @@ func (r *PrebuiltRuleResource) Create(ctx context.Context, req resource.CreateRe return } - resp.Diagnostics.Append(model.populateFromStatus(ctx, status)...) - if resp.Diagnostics.HasError() { - return - } + model.populateFromStatus(ctx, status) resp.Diagnostics.Append(resp.State.Set(ctx, model)...) } diff --git a/internal/kibana/prebuilt_rules/delete.go b/internal/kibana/prebuilt_rules/delete.go index 861f2b730..de7e022c3 100644 --- a/internal/kibana/prebuilt_rules/delete.go +++ b/internal/kibana/prebuilt_rules/delete.go @@ -3,52 +3,10 @@ package prebuilt_rules import ( "context" - "github.com/elastic/terraform-provider-elasticstack/internal/utils" - "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" ) func (r *PrebuiltRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var model prebuiltRuleModel - - resp.Diagnostics.Append(req.State.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - serverVersion, sdkDiags := r.client.ServerVersion(ctx) - resp.Diagnostics.Append(utils.FrameworkDiagsFromSDK(sdkDiags)...) - if resp.Diagnostics.HasError() { - return - } - - minVersion := version.Must(version.NewVersion("8.0.0")) - if serverVersion.LessThan(minVersion) { - resp.Diagnostics.AddError("Unsupported server version", "Prebuilt rules are not supported until Elastic Stack v8.0.0. Upgrade the target server to use this resource") - return - } - - client, err := r.client.GetKibanaOapiClient() - if err != nil { - resp.Diagnostics.AddError(err.Error(), "") - return - } - - spaceID := model.SpaceID.ValueString() - - // Disable rules that were managed by this resource - tags, tagDiags := model.getTags(ctx) - resp.Diagnostics.Append(tagDiags...) - if resp.Diagnostics.HasError() { - return - } - - if len(tags) > 0 { - resp.Diagnostics.Append(performBulkActionByTags(ctx, client, spaceID, "disable", tags)...) - if resp.Diagnostics.HasError() { - return - } - } - - // The Terraform state will be removed automatically + tflog.Info(ctx, "Delete isn't supported for elasticstack_kibana_install_prebuilt_rules") } diff --git a/internal/kibana/prebuilt_rules/models.go b/internal/kibana/prebuilt_rules/models.go index f3ecdb6b0..ab97a2dcc 100644 --- a/internal/kibana/prebuilt_rules/models.go +++ b/internal/kibana/prebuilt_rules/models.go @@ -1,12 +1,9 @@ package prebuilt_rules import ( - "bytes" "context" - "encoding/json" "fmt" "net/http" - "strings" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" @@ -18,7 +15,6 @@ import ( type prebuiltRuleModel struct { ID types.String `tfsdk:"id"` SpaceID types.String `tfsdk:"space_id"` - Tags types.List `tfsdk:"tags"` // []string RulesInstalled types.Int64 `tfsdk:"rules_installed"` RulesNotInstalled types.Int64 `tfsdk:"rules_not_installed"` RulesNotUpdated types.Int64 `tfsdk:"rules_not_updated"` @@ -27,52 +23,22 @@ type prebuiltRuleModel struct { TimelinesNotUpdated types.Int64 `tfsdk:"timelines_not_updated"` } -type prebuiltRulesStatus struct { - RulesInstalled int `json:"rules_installed"` - RulesNotInstalled int `json:"rules_not_installed"` - RulesNotUpdated int `json:"rules_not_updated"` - TimelinesInstalled int `json:"timelines_installed"` - TimelinesNotInstalled int `json:"timelines_not_installed"` - TimelinesNotUpdated int `json:"timelines_not_updated"` +func (model *prebuiltRuleModel) populateFromStatus(ctx context.Context, status *kbapi.ReadPrebuiltRulesAndTimelinesStatusResponse) { + model.RulesInstalled = types.Int64Value(int64(status.JSON200.RulesInstalled)) + model.RulesNotInstalled = types.Int64Value(int64(status.JSON200.RulesNotInstalled)) + model.RulesNotUpdated = types.Int64Value(int64(status.JSON200.RulesNotUpdated)) + model.TimelinesInstalled = types.Int64Value(int64(status.JSON200.TimelinesInstalled)) + model.TimelinesNotInstalled = types.Int64Value(int64(status.JSON200.TimelinesNotInstalled)) + model.TimelinesNotUpdated = types.Int64Value(int64(status.JSON200.TimelinesNotUpdated)) } -func (model *prebuiltRuleModel) populateFromStatus(ctx context.Context, status *prebuiltRulesStatus) diag.Diagnostics { - if status == nil { - return nil - } - - model.RulesInstalled = types.Int64Value(int64(status.RulesInstalled)) - model.RulesNotInstalled = types.Int64Value(int64(status.RulesNotInstalled)) - model.RulesNotUpdated = types.Int64Value(int64(status.RulesNotUpdated)) - model.TimelinesInstalled = types.Int64Value(int64(status.TimelinesInstalled)) - model.TimelinesNotInstalled = types.Int64Value(int64(status.TimelinesNotInstalled)) - model.TimelinesNotUpdated = types.Int64Value(int64(status.TimelinesNotUpdated)) - - return nil -} - -func (model *prebuiltRuleModel) getTags(ctx context.Context) ([]string, diag.Diagnostics) { - if model.Tags.IsNull() || model.Tags.IsUnknown() { - return nil, nil - } - - var tags []string - diags := model.Tags.ElementsAs(ctx, &tags, false) - return tags, diags -} - -func getPrebuiltRulesStatus(ctx context.Context, client *kibana_oapi.Client, spaceID string) (*prebuiltRulesStatus, diag.Diagnostics) { - var resp *kbapi.ReadPrebuiltRulesAndTimelinesStatusResponse - var err error - - if spaceID != "default" { - resp, err = client.API.ReadPrebuiltRulesAndTimelinesStatusWithResponse(ctx, func(ctx context.Context, req *http.Request) error { +func getPrebuiltRulesStatus(ctx context.Context, client *kibana_oapi.Client, spaceID string) (*kbapi.ReadPrebuiltRulesAndTimelinesStatusResponse, diag.Diagnostics) { + resp, err := client.API.ReadPrebuiltRulesAndTimelinesStatusWithResponse(ctx, func(ctx context.Context, req *http.Request) error { + if spaceID != "default" { req.Header.Set("kbn-space-id", spaceID) - return nil - }) - } else { - resp, err = client.API.ReadPrebuiltRulesAndTimelinesStatusWithResponse(ctx) - } + } + return nil + }) if err != nil { return nil, utils.FrameworkDiagFromError(err) @@ -82,33 +48,23 @@ func getPrebuiltRulesStatus(ctx context.Context, client *kibana_oapi.Client, spa return nil, utils.FrameworkDiagFromError(fmt.Errorf("failed to get prebuilt rules status: %s", resp.Status())) } - var status prebuiltRulesStatus - if err := json.Unmarshal(resp.Body, &status); err != nil { - return nil, utils.FrameworkDiagFromError(err) - } - - return &status, nil + return resp, nil } func installPrebuiltRules(ctx context.Context, client *kibana_oapi.Client, spaceID string) diag.Diagnostics { - var resp *kbapi.InstallPrebuiltRulesAndTimelinesResponse - var err error - - if spaceID != "default" { - resp, err = client.API.InstallPrebuiltRulesAndTimelinesWithResponse(ctx, func(ctx context.Context, req *http.Request) error { + resp, err := client.API.InstallPrebuiltRulesAndTimelinesWithResponse(ctx, func(ctx context.Context, req *http.Request) error { + if spaceID != "default" { req.Header.Set("kbn-space-id", spaceID) - return nil - }) - } else { - resp, err = client.API.InstallPrebuiltRulesAndTimelinesWithResponse(ctx) - } + } + return nil + }) if err != nil { return utils.FrameworkDiagFromError(err) } if resp.StatusCode() != 200 { - return utils.FrameworkDiagFromError(fmt.Errorf("failed to install prebuilt rules: %s", resp.Status())) + return utils.FrameworkDiagFromError(fmt.Errorf("failed to install prebuilt rules: %s - %s", resp.Status(), string(resp.Body))) } return nil @@ -119,127 +75,5 @@ func needsRuleUpdate(ctx context.Context, client *kibana_oapi.Client, spaceID st if diags.HasError() { return true } - return status.RulesNotInstalled >= 1 || status.RulesNotUpdated >= 1 -} - -func manageRulesByTags(ctx context.Context, client *kibana_oapi.Client, spaceID string, tags []string) diag.Diagnostics { - // Reject "all" as it's not supported by Kibana for large rule sets - if len(tags) == 1 && tags[0] == "all" { - return utils.FrameworkDiagFromError(fmt.Errorf("enabling all rules is not supported due to Kibana API limitations. Please specify specific tags to enable a subset of rules")) - } - - if len(tags) == 0 { - // If no tags specified, this resource manages no rules - this is valid - return nil - } - - // Enable rules matching the specified tags - diags := performBulkActionByTags(ctx, client, spaceID, "enable", tags) - if diags.HasError() { - return diags - } - - return nil -} - -func manageRulesTagTransition(ctx context.Context, client *kibana_oapi.Client, spaceID string, oldTags, newTags []string) diag.Diagnostics { - // Handle tag transitions for declarative behavior - - // Find tags that were removed (need to disable their rules) - var removedTags []string - for _, oldTag := range oldTags { - found := false - for _, newTag := range newTags { - if oldTag == newTag { - found = true - break - } - } - if !found { - removedTags = append(removedTags, oldTag) - } - } - - // Find tags that were added (need to enable their rules) - var addedTags []string - for _, newTag := range newTags { - found := false - for _, oldTag := range oldTags { - if newTag == oldTag { - found = true - break - } - } - if !found { - addedTags = append(addedTags, newTag) - } - } - - // Disable rules for removed tags - if len(removedTags) > 0 { - diags := performBulkActionByTags(ctx, client, spaceID, "disable", removedTags) - if diags.HasError() { - return diags - } - } - - // Enable rules for added tags - if len(addedTags) > 0 { - diags := performBulkActionByTags(ctx, client, spaceID, "enable", addedTags) - if diags.HasError() { - return diags - } - } - - return nil -} - -func performBulkActionByTags(ctx context.Context, client *kibana_oapi.Client, spaceID, action string, tags []string) diag.Diagnostics { - if len(tags) == 0 { - return nil - } - - // Reject "all" as it's not supported by Kibana for large rule sets - if len(tags) == 1 && tags[0] == "all" { - return utils.FrameworkDiagFromError(fmt.Errorf("enabling all rules is not supported due to Kibana API limitations. Please specify specific tags to enable a subset of rules")) - } - - // Build KQL query for specific tags - tagQueries := make([]string, len(tags)) - for i, tag := range tags { - tagQueries[i] = fmt.Sprintf("alert.attributes.tags:\"%s\"", tag) - } - query := strings.Join(tagQueries, " OR ") - - bulkActionBody := map[string]interface{}{ - "action": action, - "query": query, - } - - bodyJSON, err := json.Marshal(bulkActionBody) - if err != nil { - return utils.FrameworkDiagFromError(err) - } - - var resp *kbapi.PerformRulesBulkActionResponse - - if spaceID != "default" { - resp, err = client.API.PerformRulesBulkActionWithBodyWithResponse(ctx, &kbapi.PerformRulesBulkActionParams{}, "application/json", bytes.NewReader(bodyJSON), func(ctx context.Context, req *http.Request) error { - req.Header.Set("kbn-space-id", spaceID) - return nil - }) - } else { - resp, err = client.API.PerformRulesBulkActionWithBodyWithResponse(ctx, &kbapi.PerformRulesBulkActionParams{}, "application/json", bytes.NewReader(bodyJSON)) - } - - if err != nil { - return utils.FrameworkDiagFromError(err) - } - - if resp.StatusCode() != 200 { - bodyStr := string(resp.Body) - return utils.FrameworkDiagFromError(fmt.Errorf("failed to perform bulk %s action on rules: %s. Response: %s", action, resp.Status(), bodyStr)) - } - - return nil + return status.JSON200.RulesNotInstalled >= 1 || status.JSON200.RulesNotUpdated >= 1 } diff --git a/internal/kibana/prebuilt_rules/read.go b/internal/kibana/prebuilt_rules/read.go index 2e306d880..39425f3fe 100644 --- a/internal/kibana/prebuilt_rules/read.go +++ b/internal/kibana/prebuilt_rules/read.go @@ -43,10 +43,13 @@ func (r *PrebuiltRuleResource) Read(ctx context.Context, req resource.ReadReques return } - // Update computed values from status - resp.Diagnostics.Append(model.populateFromStatus(ctx, status)...) - if resp.Diagnostics.HasError() { - return + model.populateFromStatus(ctx, status) + + if needsRuleUpdate(ctx, client, spaceID) { + resp.Diagnostics.Append(installPrebuiltRules(ctx, client, spaceID)...) + if resp.Diagnostics.HasError() { + return + } } resp.Diagnostics.Append(resp.State.Set(ctx, model)...) diff --git a/internal/kibana/prebuilt_rules/resource.go b/internal/kibana/prebuilt_rules/resource.go index 7d84b6c77..53ff832a1 100644 --- a/internal/kibana/prebuilt_rules/resource.go +++ b/internal/kibana/prebuilt_rules/resource.go @@ -29,5 +29,5 @@ func (r *PrebuiltRuleResource) Configure(ctx context.Context, req resource.Confi } func (r *PrebuiltRuleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_prebuilt_rule") + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_install_prebuilt_rules") } diff --git a/internal/kibana/prebuilt_rules/schema.go b/internal/kibana/prebuilt_rules/schema.go index 59c658aec..5540ce92e 100644 --- a/internal/kibana/prebuilt_rules/schema.go +++ b/internal/kibana/prebuilt_rules/schema.go @@ -3,19 +3,16 @@ package prebuilt_rules import ( "context" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" ) func (r *PrebuiltRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: "Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines, and optionally enables/disables rules based on specified tags. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html", + Description: "Manages Elastic prebuilt detection rules. This resource installs and updates Elastic prebuilt rules and timelines. See https://www.elastic.co/guide/en/security/current/prebuilt-rules.html", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, @@ -33,14 +30,6 @@ func (r *PrebuiltRuleResource) Schema(_ context.Context, _ resource.SchemaReques stringplanmodifier.RequiresReplace(), }, }, - "tags": schema.ListAttribute{ - Description: "A list of tag names to filter prebuilt rules for enabling/disabling. Use ['all'] to enable all prebuilt rules, or an empty list to install but not enable any rules.", - ElementType: types.StringType, - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(0), - }, - }, "rules_installed": schema.Int64Attribute{ Description: "Number of prebuilt rules that are installed.", Computed: true, diff --git a/internal/kibana/prebuilt_rules/update.go b/internal/kibana/prebuilt_rules/update.go index 09983af07..30e68061a 100644 --- a/internal/kibana/prebuilt_rules/update.go +++ b/internal/kibana/prebuilt_rules/update.go @@ -38,7 +38,6 @@ func (r *PrebuiltRuleResource) Update(ctx context.Context, req resource.UpdateRe spaceID := model.SpaceID.ValueString() - // Check if we need to install/update rules if needsRuleUpdate(ctx, client, spaceID) { resp.Diagnostics.Append(installPrebuiltRules(ctx, client, spaceID)...) if resp.Diagnostics.HasError() { @@ -46,36 +45,13 @@ func (r *PrebuiltRuleResource) Update(ctx context.Context, req resource.UpdateRe } } - // Handle tag transitions for declarative behavior - newTags, newTagDiags := model.getTags(ctx) - resp.Diagnostics.Append(newTagDiags...) - if resp.Diagnostics.HasError() { - return - } - - oldTags, oldTagDiags := priorModel.getTags(ctx) - resp.Diagnostics.Append(oldTagDiags...) - if resp.Diagnostics.HasError() { - return - } - - // Use transition logic to handle tag changes declaratively - resp.Diagnostics.Append(manageRulesTagTransition(ctx, client, spaceID, oldTags, newTags)...) - if resp.Diagnostics.HasError() { - return - } - - // Read the current status to populate computed attributes status, statusDiags := getPrebuiltRulesStatus(ctx, client, spaceID) resp.Diagnostics.Append(statusDiags...) if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append(model.populateFromStatus(ctx, status)...) - if resp.Diagnostics.HasError() { - return - } + model.populateFromStatus(ctx, status) resp.Diagnostics.Append(resp.State.Set(ctx, model)...) } From 517a2620b202ddb99c8634d5830dccc7650e43c4 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Thu, 11 Sep 2025 11:18:05 -0400 Subject: [PATCH 10/10] fix resource name in test --- internal/kibana/prebuilt_rules/acc_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/kibana/prebuilt_rules/acc_test.go b/internal/kibana/prebuilt_rules/acc_test.go index 3479e5dc9..30670c016 100644 --- a/internal/kibana/prebuilt_rules/acc_test.go +++ b/internal/kibana/prebuilt_rules/acc_test.go @@ -20,13 +20,13 @@ func TestAccResourcePrebuiltRules(t *testing.T) { SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules), Config: testAccPrebuiltRuleConfigBasic(), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("elasticstack_kibana_prebuilt_rule.test", "space_id", "default"), - resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "rules_installed"), - resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "rules_not_installed"), - resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "rules_not_updated"), - resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "timelines_installed"), - resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "timelines_not_installed"), - resource.TestCheckResourceAttrSet("elasticstack_kibana_prebuilt_rule.test", "timelines_not_updated"), + resource.TestCheckResourceAttr("elasticstack_kibana_install_prebuilt_rules.test", "space_id", "default"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_not_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "rules_not_updated"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_not_installed"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_install_prebuilt_rules.test", "timelines_not_updated"), ), }, },