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)) 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/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/internal/kibana/prebuilt_rules/acc_test.go b/internal/kibana/prebuilt_rules/acc_test.go new file mode 100644 index 000000000..30670c016 --- /dev/null +++ b/internal/kibana/prebuilt_rules/acc_test.go @@ -0,0 +1,42 @@ +package prebuilt_rules_test + +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" +) + +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{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minVersionPrebuiltRules), + Config: testAccPrebuiltRuleConfigBasic(), + Check: resource.ComposeTestCheckFunc( + 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"), + ), + }, + }, + }) +} + +func testAccPrebuiltRuleConfigBasic() string { + return ` +resource "elasticstack_kibana_install_prebuilt_rules" "test" { + space_id = "default" +} +` +} diff --git a/internal/kibana/prebuilt_rules/create.go b/internal/kibana/prebuilt_rules/create.go new file mode 100644 index 000000000..ffed50423 --- /dev/null +++ b/internal/kibana/prebuilt_rules/create.go @@ -0,0 +1,58 @@ +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" +) + +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 + } + + 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() + + // Install/update prebuilt rules and timelines + resp.Diagnostics.Append(installPrebuiltRules(ctx, client, spaceID)...) + 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 + } + + 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 new file mode 100644 index 000000000..de7e022c3 --- /dev/null +++ b/internal/kibana/prebuilt_rules/delete.go @@ -0,0 +1,12 @@ +package prebuilt_rules + +import ( + "context" + + "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) { + 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 new file mode 100644 index 000000000..ab97a2dcc --- /dev/null +++ b/internal/kibana/prebuilt_rules/models.go @@ -0,0 +1,79 @@ +package prebuilt_rules + +import ( + "context" + "fmt" + "net/http" + + "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"` + 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"` +} + +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 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 + }) + + 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())) + } + + return resp, nil +} + +func installPrebuiltRules(ctx context.Context, client *kibana_oapi.Client, spaceID string) diag.Diagnostics { + 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 + }) + + if err != nil { + return utils.FrameworkDiagFromError(err) + } + + if resp.StatusCode() != 200 { + return utils.FrameworkDiagFromError(fmt.Errorf("failed to install prebuilt rules: %s - %s", resp.Status(), string(resp.Body))) + } + + 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.JSON200.RulesNotInstalled >= 1 || status.JSON200.RulesNotUpdated >= 1 +} diff --git a/internal/kibana/prebuilt_rules/read.go b/internal/kibana/prebuilt_rules/read.go new file mode 100644 index 000000000..39425f3fe --- /dev/null +++ b/internal/kibana/prebuilt_rules/read.go @@ -0,0 +1,56 @@ +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" +) + +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 + } + + 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.ID.ValueString() + + // Get current status + status, statusDiags := getPrebuiltRulesStatus(ctx, client, spaceID) + resp.Diagnostics.Append(statusDiags...) + 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 new file mode 100644 index 000000000..53ff832a1 --- /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_install_prebuilt_rules") +} diff --git a/internal/kibana/prebuilt_rules/schema.go b/internal/kibana/prebuilt_rules/schema.go new file mode 100644 index 000000000..5540ce92e --- /dev/null +++ b/internal/kibana/prebuilt_rules/schema.go @@ -0,0 +1,59 @@ +package prebuilt_rules + +import ( + "context" + + "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" +) + +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. 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(), + }, + }, + "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..30e68061a --- /dev/null +++ b/internal/kibana/prebuilt_rules/update.go @@ -0,0 +1,57 @@ +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" +) + +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 + } + + 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() + + if needsRuleUpdate(ctx, client, spaceID) { + resp.Diagnostics.Append(installPrebuiltRules(ctx, client, spaceID)...) + if resp.Diagnostics.HasError() { + return + } + } + + status, statusDiags := getPrebuiltRulesStatus(ctx, client, spaceID) + resp.Diagnostics.Append(statusDiags...) + if resp.Diagnostics.HasError() { + return + } + + model.populateFromStatus(ctx, status) + + 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, } }