From 7b5909aa8c13831675327a0ccda56410823c334b Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 10:26:00 -0700 Subject: [PATCH 01/10] Add security exception list resource --- .../security_exception_list/acc_test.go | 136 +++++++++++++ .../kibana/security_exception_list/create.go | 181 ++++++++++++++++++ .../kibana/security_exception_list/delete.go | 34 ++++ .../kibana/security_exception_list/models.go | 24 +++ .../kibana/security_exception_list/read.go | 52 +++++ .../resource-description.md | 3 + .../security_exception_list/resource.go | 40 ++++ .../kibana/security_exception_list/schema.go | 124 ++++++++++++ .../create/exception_list.tf | 43 +++++ .../update/exception_list.tf | 43 +++++ .../create/exception_list.tf | 50 +++++ .../update/exception_list.tf | 50 +++++ .../kibana/security_exception_list/update.go | 128 +++++++++++++ 13 files changed, 908 insertions(+) create mode 100644 internal/kibana/security_exception_list/acc_test.go create mode 100644 internal/kibana/security_exception_list/create.go create mode 100644 internal/kibana/security_exception_list/delete.go create mode 100644 internal/kibana/security_exception_list/models.go create mode 100644 internal/kibana/security_exception_list/read.go create mode 100644 internal/kibana/security_exception_list/resource-description.md create mode 100644 internal/kibana/security_exception_list/resource.go create mode 100644 internal/kibana/security_exception_list/schema.go create mode 100644 internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/create/exception_list.tf create mode 100644 internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/update/exception_list.tf create mode 100644 internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/create/exception_list.tf create mode 100644 internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/update/exception_list.tf create mode 100644 internal/kibana/security_exception_list/update.go diff --git a/internal/kibana/security_exception_list/acc_test.go b/internal/kibana/security_exception_list/acc_test.go new file mode 100644 index 000000000..89059330d --- /dev/null +++ b/internal/kibana/security_exception_list/acc_test.go @@ -0,0 +1,136 @@ +package security_exception_list_test + +import ( + "fmt" + "testing" + + "github.com/elastic/terraform-provider-elasticstack/internal/acctest" + "github.com/elastic/terraform-provider-elasticstack/internal/versionutils" + "github.com/google/uuid" + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +var minExceptionListAPISupport = version.Must(version.NewVersion("7.9.0")) + +func TestAccResourceExceptionList(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport), + ProtoV6ProviderFactories: acctest.Providers, + ConfigDirectory: acctest.NamedTestCaseDirectory("create"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable("test-exception-list"), + "name": config.StringVariable("Test Exception List"), + "description": config.StringVariable("Test exception list for acceptance tests"), + "type": config.StringVariable("detection"), + "namespace_type": config.StringVariable("single"), + "tags": config.ListVariable(config.StringVariable("test")), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "list_id", "test-exception-list"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "name", "Test Exception List"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "description", "Test exception list for acceptance tests"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "type", "detection"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "namespace_type", "single"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "tags.0", "test"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_exception_list.test", "id"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_exception_list.test", "created_at"), + resource.TestCheckResourceAttrSet("elasticstack_kibana_security_exception_list.test", "created_by"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport), + ProtoV6ProviderFactories: acctest.Providers, + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable("test-exception-list"), + "name": config.StringVariable("Test Exception List Updated"), + "description": config.StringVariable("Updated description"), + "type": config.StringVariable("detection"), + "namespace_type": config.StringVariable("single"), + "tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("updated")), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "name", "Test Exception List Updated"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "description", "Updated description"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "tags.0", "test"), + resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "tags.1", "updated"), + ), + }, + }, + }) +} + +func TestAccResourceExceptionListWithSpace(t *testing.T) { + resourceName := "elasticstack_kibana_security_exception_list.test" + spaceResourceName := "elasticstack_kibana_space.test" + spaceID := fmt.Sprintf("test-space-%s", uuid.New().String()[:8]) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport), + ConfigDirectory: acctest.NamedTestCaseDirectory("create"), + ConfigVariables: config.Variables{ + "space_id": config.StringVariable(spaceID), + "list_id": config.StringVariable("test-exception-list-space"), + "name": config.StringVariable("Test Exception List in Space"), + "description": config.StringVariable("Test exception list in custom space"), + "type": config.StringVariable("detection"), + "namespace_type": config.StringVariable("single"), + "tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("space")), + }, + Check: resource.ComposeTestCheckFunc( + // Check space attributes + resource.TestCheckResourceAttr(spaceResourceName, "space_id", spaceID), + resource.TestCheckResourceAttr(spaceResourceName, "name", "Test Space for Exception Lists"), + + // Check exception list attributes + resource.TestCheckResourceAttr(resourceName, "space_id", spaceID), + resource.TestCheckResourceAttr(resourceName, "list_id", "test-exception-list-space"), + resource.TestCheckResourceAttr(resourceName, "name", "Test Exception List in Space"), + resource.TestCheckResourceAttr(resourceName, "description", "Test exception list in custom space"), + resource.TestCheckResourceAttr(resourceName, "type", "detection"), + resource.TestCheckResourceAttr(resourceName, "namespace_type", "single"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "test"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "space"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "created_by"), + ), + }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport), + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "space_id": config.StringVariable(spaceID), + "list_id": config.StringVariable("test-exception-list-space"), + "name": config.StringVariable("Test Exception List in Space Updated"), + "description": config.StringVariable("Updated description in space"), + "type": config.StringVariable("detection"), + "namespace_type": config.StringVariable("single"), + "tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("space"), config.StringVariable("updated")), + }, + Check: resource.ComposeTestCheckFunc( + // Check space attributes remain the same + resource.TestCheckResourceAttr(spaceResourceName, "space_id", spaceID), + resource.TestCheckResourceAttr(spaceResourceName, "name", "Test Space for Exception Lists"), + + // Check updated exception list attributes + resource.TestCheckResourceAttr(resourceName, "space_id", spaceID), + resource.TestCheckResourceAttr(resourceName, "name", "Test Exception List in Space Updated"), + resource.TestCheckResourceAttr(resourceName, "description", "Updated description in space"), + resource.TestCheckResourceAttr(resourceName, "tags.0", "test"), + resource.TestCheckResourceAttr(resourceName, "tags.1", "space"), + resource.TestCheckResourceAttr(resourceName, "tags.2", "updated"), + ), + }, + }, + }) +} diff --git a/internal/kibana/security_exception_list/create.go b/internal/kibana/security_exception_list/create.go new file mode 100644 index 000000000..ff26fa6f4 --- /dev/null +++ b/internal/kibana/security_exception_list/create.go @@ -0,0 +1,181 @@ +package security_exception_list + +import ( + "context" + "encoding/json" + + "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/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *ExceptionListResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ExceptionListModel + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Build the request body + body := kbapi.CreateExceptionListJSONRequestBody{ + ListId: (*kbapi.SecurityExceptionsAPIExceptionListHumanId)(plan.ListID.ValueStringPointer()), + Name: kbapi.SecurityExceptionsAPIExceptionListName(plan.Name.ValueString()), + Description: kbapi.SecurityExceptionsAPIExceptionListDescription(plan.Description.ValueString()), + Type: kbapi.SecurityExceptionsAPIExceptionListType(plan.Type.ValueString()), + } + + // Set optional namespace_type + if utils.IsKnown(plan.NamespaceType) { + nsType := kbapi.SecurityExceptionsAPIExceptionNamespaceType(plan.NamespaceType.ValueString()) + body.NamespaceType = &nsType + } + + // Set optional os_types + if utils.IsKnown(plan.OsTypes) { + var osTypes []string + diags := plan.OsTypes.ElementsAs(ctx, &osTypes, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if len(osTypes) > 0 { + osTypesArray := make(kbapi.SecurityExceptionsAPIExceptionListOsTypeArray, len(osTypes)) + for i, osType := range osTypes { + osTypesArray[i] = kbapi.SecurityExceptionsAPIExceptionListOsType(osType) + } + body.OsTypes = &osTypesArray + } + } + + // Set optional tags + if utils.IsKnown(plan.Tags) { + var tags []string + diags := plan.Tags.ElementsAs(ctx, &tags, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if len(tags) > 0 { + tagsArray := kbapi.SecurityExceptionsAPIExceptionListTags(tags) + body.Tags = &tagsArray + } + } + + // Set optional meta + if utils.IsKnown(plan.Meta) { + var meta kbapi.SecurityExceptionsAPIExceptionListMeta + if err := json.Unmarshal([]byte(plan.Meta.ValueString()), &meta); err != nil { + resp.Diagnostics.AddError("Failed to parse meta JSON", err.Error()) + return + } + body.Meta = &meta + } + + // Create the exception list + createResp, diags := kibana_oapi.CreateExceptionList(ctx, client, plan.SpaceID.ValueString(), body) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if createResp == nil || createResp.JSON200 == nil { + resp.Diagnostics.AddError("Failed to create exception list", "API returned empty response") + return + } + + /* + * In create/update paths we typically follow the write operation with a read, and then set the state from the read. + * We want to avoid a dirty plan immediately after an apply. + */ + // Read back the created resource to get the final state + readParams := &kbapi.ReadExceptionListParams{ + Id: (*kbapi.SecurityExceptionsAPIExceptionListId)(&createResp.JSON200.Id), + } + + readResp, diags := kibana_oapi.GetExceptionList(ctx, client, plan.SpaceID.ValueString(), readParams) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readResp == nil || readResp.JSON200 == nil { + resp.State.RemoveResource(ctx) + resp.Diagnostics.AddError("Failed to fetch exception list", "API returned empty response") + return + } + + // Update state with read response + diags = r.updateStateFromAPIResponse(ctx, &plan, readResp.JSON200) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +func (r *ExceptionListResource) updateStateFromAPIResponse(ctx context.Context, model *ExceptionListModel, apiResp *kbapi.SecurityExceptionsAPIExceptionList) diag.Diagnostics { + var diags diag.Diagnostics + + model.ID = types.StringValue(string(apiResp.Id)) + model.ListID = types.StringValue(string(apiResp.ListId)) + model.Name = types.StringValue(string(apiResp.Name)) + model.Description = types.StringValue(string(apiResp.Description)) + model.Type = types.StringValue(string(apiResp.Type)) + model.NamespaceType = types.StringValue(string(apiResp.NamespaceType)) + model.CreatedAt = types.StringValue(apiResp.CreatedAt.Format("2006-01-02T15:04:05.000Z")) + model.CreatedBy = types.StringValue(apiResp.CreatedBy) + model.UpdatedAt = types.StringValue(apiResp.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) + model.UpdatedBy = types.StringValue(apiResp.UpdatedBy) + model.Immutable = types.BoolValue(apiResp.Immutable) + model.TieBreakerID = types.StringValue(apiResp.TieBreakerId) + + // Set optional os_types + if apiResp.OsTypes != nil && len(*apiResp.OsTypes) > 0 { + // osTypes := make([]string, len(*apiResp.OsTypes)) + // for i, osType := range *apiResp.OsTypes { + // osTypes[i] = string(osType) + // } + // list, d := types.ListValueFrom(ctx, types.StringType, osTypes) + list, d := types.ListValueFrom(ctx, types.StringType, apiResp.OsTypes) + diags.Append(d...) + model.OsTypes = list + } else { + model.OsTypes = types.ListNull(types.StringType) + } + + // Set optional tags + if apiResp.Tags != nil && len(*apiResp.Tags) > 0 { + list, d := types.ListValueFrom(ctx, types.StringType, *apiResp.Tags) + diags.Append(d...) + model.Tags = list + } else { + model.Tags = types.ListNull(types.StringType) + } + + // Set optional meta + if apiResp.Meta != nil { + metaJSON, err := json.Marshal(apiResp.Meta) + if err != nil { + diags.AddError("Failed to serialize meta", err.Error()) + return diags + } + model.Meta = types.StringValue(string(metaJSON)) + } else { + model.Meta = types.StringNull() + } + + return diags +} diff --git a/internal/kibana/security_exception_list/delete.go b/internal/kibana/security_exception_list/delete.go new file mode 100644 index 000000000..0c2b34b96 --- /dev/null +++ b/internal/kibana/security_exception_list/delete.go @@ -0,0 +1,34 @@ +package security_exception_list + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *ExceptionListResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ExceptionListModel + + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Delete by ID + id := kbapi.SecurityExceptionsAPIExceptionListId(state.ID.ValueString()) + params := &kbapi.DeleteExceptionListParams{ + Id: &id, + } + + diags = kibana_oapi.DeleteExceptionList(ctx, client, state.SpaceID.ValueString(), params) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/kibana/security_exception_list/models.go b/internal/kibana/security_exception_list/models.go new file mode 100644 index 000000000..5e97aa9f9 --- /dev/null +++ b/internal/kibana/security_exception_list/models.go @@ -0,0 +1,24 @@ +package security_exception_list + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type ExceptionListModel struct { + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + ListID types.String `tfsdk:"list_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Type types.String `tfsdk:"type"` + NamespaceType types.String `tfsdk:"namespace_type"` + OsTypes types.List `tfsdk:"os_types"` + Tags types.List `tfsdk:"tags"` + Meta types.String `tfsdk:"meta"` + CreatedAt types.String `tfsdk:"created_at"` + CreatedBy types.String `tfsdk:"created_by"` + UpdatedAt types.String `tfsdk:"updated_at"` + UpdatedBy types.String `tfsdk:"updated_by"` + Immutable types.Bool `tfsdk:"immutable"` + TieBreakerID types.String `tfsdk:"tie_breaker_id"` +} diff --git a/internal/kibana/security_exception_list/read.go b/internal/kibana/security_exception_list/read.go new file mode 100644 index 000000000..1ace40cb5 --- /dev/null +++ b/internal/kibana/security_exception_list/read.go @@ -0,0 +1,52 @@ +package security_exception_list + +import ( + "context" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func (r *ExceptionListResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ExceptionListModel + + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Read by ID + id := kbapi.SecurityExceptionsAPIExceptionListId(state.ID.ValueString()) + params := &kbapi.ReadExceptionListParams{ + Id: &id, + } + + readResp, diags := kibana_oapi.GetExceptionList(ctx, client, state.SpaceID.ValueString(), params) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readResp == nil || readResp.JSON200 == nil { + resp.State.RemoveResource(ctx) + return + } + + // Update state with response + diags = r.updateStateFromAPIResponse(ctx, &state, readResp.JSON200) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/kibana/security_exception_list/resource-description.md b/internal/kibana/security_exception_list/resource-description.md new file mode 100644 index 000000000..d773a038b --- /dev/null +++ b/internal/kibana/security_exception_list/resource-description.md @@ -0,0 +1,3 @@ +Manages a Kibana Exception List. Exception lists are containers for exception items used to prevent security rules from generating alerts. + +See the [Kibana Exceptions API documentation](https://www.elastic.co/docs/api/doc/kibana/group/endpoint-security-exceptions-api) for more details. diff --git a/internal/kibana/security_exception_list/resource.go b/internal/kibana/security_exception_list/resource.go new file mode 100644 index 000000000..03b5c3a71 --- /dev/null +++ b/internal/kibana/security_exception_list/resource.go @@ -0,0 +1,40 @@ +package security_exception_list + +import ( + "context" + "fmt" + + "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var ( + _ resource.Resource = &ExceptionListResource{} + _ resource.ResourceWithConfigure = &ExceptionListResource{} + _ resource.ResourceWithImportState = &ExceptionListResource{} +) + +// NewResource is a helper function to simplify the provider implementation. +func NewResource() resource.Resource { + return &ExceptionListResource{} +} + +type ExceptionListResource struct { + client *clients.ApiClient +} + +func (r *ExceptionListResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := clients.ConvertProviderData(req.ProviderData) + resp.Diagnostics.Append(diags...) + r.client = client +} + +// Metadata returns the provider type name. +func (r *ExceptionListResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s", req.ProviderTypeName, "kibana_security_exception_list") +} + +func (r *ExceptionListResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} diff --git a/internal/kibana/security_exception_list/schema.go b/internal/kibana/security_exception_list/schema.go new file mode 100644 index 000000000..8d1f934a6 --- /dev/null +++ b/internal/kibana/security_exception_list/schema.go @@ -0,0 +1,124 @@ +package security_exception_list + +import ( + "context" + _ "embed" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" +) + +//go:embed resource-description.md +var exceptionListResourceDescription string + +func (r *ExceptionListResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: exceptionListResourceDescription, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the exception list (auto-generated by Kibana).", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "space_id": schema.StringAttribute{ + MarkdownDescription: "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(), + }, + }, + "list_id": schema.StringAttribute{ + MarkdownDescription: "The exception list's human readable string identifier.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the exception list.", + Required: true, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "Describes the exception list.", + Required: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "The type of exception list. Can be one of: `detection`, `endpoint`, `endpoint_trusted_apps`, `endpoint_events`, `endpoint_host_isolation_exceptions`, `endpoint_blocklists`.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + "detection", + "endpoint", + "endpoint_trusted_apps", + "endpoint_events", + "endpoint_host_isolation_exceptions", + "endpoint_blocklists", + ), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "namespace_type": schema.StringAttribute{ + MarkdownDescription: "Determines whether the exception list is available in all Kibana spaces or just the space in which it is created. Can be `single` (default) or `agnostic`.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("single"), + Validators: []validator.String{ + stringvalidator.OneOf("single", "agnostic"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "os_types": schema.ListAttribute{ + MarkdownDescription: "Array of OS types for which the exceptions apply. Valid values: `linux`, `macos`, `windows`.", + Optional: true, + ElementType: types.StringType, + }, + "tags": schema.ListAttribute{ + MarkdownDescription: "String array containing words and phrases to help categorize exception containers.", + Optional: true, + ElementType: types.StringType, + }, + "meta": schema.StringAttribute{ + MarkdownDescription: "Placeholder for metadata about the list container as JSON string.", + Optional: true, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp of when the exception list was created.", + Computed: true, + }, + "created_by": schema.StringAttribute{ + MarkdownDescription: "The user who created the exception list.", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp of when the exception list was last updated.", + Computed: true, + }, + "updated_by": schema.StringAttribute{ + MarkdownDescription: "The user who last updated the exception list.", + Computed: true, + }, + "immutable": schema.BoolAttribute{ + MarkdownDescription: "Whether the exception list is immutable.", + Computed: true, + }, + "tie_breaker_id": schema.StringAttribute{ + MarkdownDescription: "Field used in search to ensure all containers are sorted and returned correctly.", + Computed: true, + }, + }, + } +} diff --git a/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/create/exception_list.tf b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/create/exception_list.tf new file mode 100644 index 000000000..06adaccef --- /dev/null +++ b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/create/exception_list.tf @@ -0,0 +1,43 @@ +variable "list_id" { + description = "The exception list ID" + type = string +} + +variable "name" { + description = "The exception list name" + type = string +} + +variable "description" { + description = "The exception list description" + type = string +} + +variable "type" { + description = "The exception list type" + type = string +} + +variable "namespace_type" { + description = "The namespace type" + type = string +} + +variable "tags" { + description = "Tags for the exception list" + type = list(string) +} + +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_security_exception_list" "test" { + list_id = var.list_id + name = var.name + description = var.description + type = var.type + namespace_type = var.namespace_type + tags = var.tags +} diff --git a/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/update/exception_list.tf b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/update/exception_list.tf new file mode 100644 index 000000000..06adaccef --- /dev/null +++ b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionList/update/exception_list.tf @@ -0,0 +1,43 @@ +variable "list_id" { + description = "The exception list ID" + type = string +} + +variable "name" { + description = "The exception list name" + type = string +} + +variable "description" { + description = "The exception list description" + type = string +} + +variable "type" { + description = "The exception list type" + type = string +} + +variable "namespace_type" { + description = "The namespace type" + type = string +} + +variable "tags" { + description = "Tags for the exception list" + type = list(string) +} + +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_security_exception_list" "test" { + list_id = var.list_id + name = var.name + description = var.description + type = var.type + namespace_type = var.namespace_type + tags = var.tags +} diff --git a/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/create/exception_list.tf b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/create/exception_list.tf new file mode 100644 index 000000000..c9122eb0f --- /dev/null +++ b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/create/exception_list.tf @@ -0,0 +1,50 @@ +variable "space_id" { + description = "The Kibana space ID" + type = string +} + +variable "list_id" { + description = "The exception list ID" + type = string +} + +variable "name" { + description = "The exception list name" + type = string +} + +variable "description" { + description = "The exception list description" + type = string +} + +variable "type" { + description = "The exception list type" + type = string +} + +variable "namespace_type" { + description = "The namespace type" + type = string +} + +variable "tags" { + description = "Tags for the exception list" + type = list(string) +} + +resource "elasticstack_kibana_space" "test" { + space_id = var.space_id + name = "Test Space for Exception Lists" + description = "Space for testing exception lists" +} + +resource "elasticstack_kibana_security_exception_list" "test" { + space_id = elasticstack_kibana_space.test.space_id + list_id = var.list_id + name = var.name + description = var.description + type = var.type + namespace_type = var.namespace_type + tags = var.tags +} diff --git a/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/update/exception_list.tf b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/update/exception_list.tf new file mode 100644 index 000000000..c9122eb0f --- /dev/null +++ b/internal/kibana/security_exception_list/testdata/TestAccResourceExceptionListWithSpace/update/exception_list.tf @@ -0,0 +1,50 @@ +variable "space_id" { + description = "The Kibana space ID" + type = string +} + +variable "list_id" { + description = "The exception list ID" + type = string +} + +variable "name" { + description = "The exception list name" + type = string +} + +variable "description" { + description = "The exception list description" + type = string +} + +variable "type" { + description = "The exception list type" + type = string +} + +variable "namespace_type" { + description = "The namespace type" + type = string +} + +variable "tags" { + description = "Tags for the exception list" + type = list(string) +} + +resource "elasticstack_kibana_space" "test" { + space_id = var.space_id + name = "Test Space for Exception Lists" + description = "Space for testing exception lists" +} + +resource "elasticstack_kibana_security_exception_list" "test" { + space_id = elasticstack_kibana_space.test.space_id + list_id = var.list_id + name = var.name + description = var.description + type = var.type + namespace_type = var.namespace_type + tags = var.tags +} diff --git a/internal/kibana/security_exception_list/update.go b/internal/kibana/security_exception_list/update.go new file mode 100644 index 000000000..97abd0d5d --- /dev/null +++ b/internal/kibana/security_exception_list/update.go @@ -0,0 +1,128 @@ +package security_exception_list + +import ( + "context" + "encoding/json" + + "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/resource" +) + +func (r *ExceptionListResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ExceptionListModel + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.GetKibanaOapiClient() + if err != nil { + resp.Diagnostics.AddError("Failed to get Kibana client", err.Error()) + return + } + + // Build the update request body + id := kbapi.SecurityExceptionsAPIExceptionListId(plan.ID.ValueString()) + body := kbapi.UpdateExceptionListJSONRequestBody{ + Id: &id, + Name: kbapi.SecurityExceptionsAPIExceptionListName(plan.Name.ValueString()), + Description: kbapi.SecurityExceptionsAPIExceptionListDescription(plan.Description.ValueString()), + // Type is required by the API even though it has RequiresReplace in the schema + // The API will reject updates without this field, even though the value cannot change + Type: kbapi.SecurityExceptionsAPIExceptionListType(plan.Type.ValueString()), + } + + // Set optional namespace_type (should not change, but include it) + if utils.IsKnown(plan.NamespaceType) { + nsType := kbapi.SecurityExceptionsAPIExceptionNamespaceType(plan.NamespaceType.ValueString()) + body.NamespaceType = &nsType + } + + // Set optional os_types + if utils.IsKnown(plan.OsTypes) { + var osTypes []string + diags := plan.OsTypes.ElementsAs(ctx, &osTypes, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if len(osTypes) > 0 { + osTypesArray := make(kbapi.SecurityExceptionsAPIExceptionListOsTypeArray, len(osTypes)) + for i, osType := range osTypes { + osTypesArray[i] = kbapi.SecurityExceptionsAPIExceptionListOsType(osType) + } + body.OsTypes = &osTypesArray + } + } + + // Set optional tags + if utils.IsKnown(plan.Tags) { + var tags []string + diags := plan.Tags.ElementsAs(ctx, &tags, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if len(tags) > 0 { + tagsArray := kbapi.SecurityExceptionsAPIExceptionListTags(tags) + body.Tags = &tagsArray + } + } + + // Set optional meta + if utils.IsKnown(plan.Meta) { + var meta kbapi.SecurityExceptionsAPIExceptionListMeta + if err := json.Unmarshal([]byte(plan.Meta.ValueString()), &meta); err != nil { + resp.Diagnostics.AddError("Failed to parse meta JSON", err.Error()) + return + } + body.Meta = &meta + } + + // Update the exception list + updateResp, diags := kibana_oapi.UpdateExceptionList(ctx, client, plan.SpaceID.ValueString(), body) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if updateResp == nil || updateResp.JSON200 == nil { + resp.Diagnostics.AddError("Failed to update exception list", "API returned empty response") + return + } + + /* + * In create/update paths we typically follow the write operation with a read, and then set the state from the read. + * We want to avoid a dirty plan immediately after an apply. + */ + // Read back the updated resource to get the final state + readParams := &kbapi.ReadExceptionListParams{ + Id: (*kbapi.SecurityExceptionsAPIExceptionListId)(&updateResp.JSON200.Id), + } + + readResp, diags := kibana_oapi.GetExceptionList(ctx, client, plan.SpaceID.ValueString(), readParams) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if readResp == nil || readResp.JSON200 == nil { + resp.State.RemoveResource(ctx) + resp.Diagnostics.AddError("Failed to fetch exception list", "API returned empty response") + return + } + + // Update state with read response + diags = r.updateStateFromAPIResponse(ctx, &plan, readResp.JSON200) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} From f29832be16bb76eb1b64b7765f6082cfd2ae6a84 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 10:27:53 -0700 Subject: [PATCH 02/10] Add exception list examples --- .../resource.tf | 9 +++++++++ .../resource_endpoint.tf | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 examples/resources/elasticstack_kibana_security_exception_list/resource.tf create mode 100644 examples/resources/elasticstack_kibana_security_exception_list/resource_endpoint.tf diff --git a/examples/resources/elasticstack_kibana_security_exception_list/resource.tf b/examples/resources/elasticstack_kibana_security_exception_list/resource.tf new file mode 100644 index 000000000..70c9805f0 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_exception_list/resource.tf @@ -0,0 +1,9 @@ +resource "elasticstack_kibana_security_exception_list" "example" { + list_id = "my-detection-exception-list" + name = "My Detection Exception List" + description = "List of exceptions for security detection rules" + type = "detection" + namespace_type = "single" + + tags = ["security", "detections"] +} diff --git a/examples/resources/elasticstack_kibana_security_exception_list/resource_endpoint.tf b/examples/resources/elasticstack_kibana_security_exception_list/resource_endpoint.tf new file mode 100644 index 000000000..2aaa604f3 --- /dev/null +++ b/examples/resources/elasticstack_kibana_security_exception_list/resource_endpoint.tf @@ -0,0 +1,10 @@ +resource "elasticstack_kibana_security_exception_list" "endpoint" { + list_id = "my-endpoint-exception-list" + name = "My Endpoint Exception List" + description = "List of endpoint exceptions" + type = "endpoint" + namespace_type = "agnostic" + + os_types = ["linux", "windows", "macos"] + tags = ["endpoint", "security"] +} From e385ba4882b409842172df87d5247401124fbdc0 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 12:56:27 -0700 Subject: [PATCH 03/10] Add exceptions client helper --- internal/clients/kibana_oapi/exceptions.go | 138 +++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 internal/clients/kibana_oapi/exceptions.go diff --git a/internal/clients/kibana_oapi/exceptions.go b/internal/clients/kibana_oapi/exceptions.go new file mode 100644 index 000000000..8c1d39464 --- /dev/null +++ b/internal/clients/kibana_oapi/exceptions.go @@ -0,0 +1,138 @@ +package kibana_oapi + +import ( + "context" + "net/http" + + "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/diagutil" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// GetExceptionList reads an exception list from the API by ID or list_id +func GetExceptionList(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadExceptionListParams) (*kbapi.ReadExceptionListResponse, diag.Diagnostics) { + resp, err := client.API.ReadExceptionListWithResponse(ctx, kbapi.SpaceId(spaceId), params) + if err != nil { + return nil, diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + case http.StatusNotFound: + return nil, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// CreateExceptionList creates a new exception list. +func CreateExceptionList(ctx context.Context, client *Client, spaceId string, body kbapi.CreateExceptionListJSONRequestBody) (*kbapi.CreateExceptionListResponse, diag.Diagnostics) { + resp, err := client.API.CreateExceptionListWithResponse(ctx, kbapi.SpaceId(spaceId), body) + if err != nil { + return nil, diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// UpdateExceptionList updates an existing exception list. +func UpdateExceptionList(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateExceptionListJSONRequestBody) (*kbapi.UpdateExceptionListResponse, diag.Diagnostics) { + resp, err := client.API.UpdateExceptionListWithResponse(ctx, kbapi.SpaceId(spaceId), body) + if err != nil { + return nil, diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// DeleteExceptionList deletes an existing exception list. +func DeleteExceptionList(ctx context.Context, client *Client, spaceId string, params *kbapi.DeleteExceptionListParams) diag.Diagnostics { + resp, err := client.API.DeleteExceptionListWithResponse(ctx, kbapi.SpaceId(spaceId), params) + if err != nil { + return diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return nil + case http.StatusNotFound: + return nil + default: + return reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// GetExceptionListItem reads an exception list item from the API by ID or item_id +func GetExceptionListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadExceptionListItemParams) (*kbapi.ReadExceptionListItemResponse, diag.Diagnostics) { + resp, err := client.API.ReadExceptionListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params) + if err != nil { + return nil, diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + case http.StatusNotFound: + return nil, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// CreateExceptionListItem creates a new exception list item. +func CreateExceptionListItem(ctx context.Context, client *Client, spaceId string, body kbapi.CreateExceptionListItemJSONRequestBody) (*kbapi.CreateExceptionListItemResponse, diag.Diagnostics) { + resp, err := client.API.CreateExceptionListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body) + if err != nil { + return nil, diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// UpdateExceptionListItem updates an existing exception list item. +func UpdateExceptionListItem(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateExceptionListItemJSONRequestBody) (*kbapi.UpdateExceptionListItemResponse, diag.Diagnostics) { + resp, err := client.API.UpdateExceptionListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body) + if err != nil { + return nil, diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return resp, nil + default: + return nil, reportUnknownError(resp.StatusCode(), resp.Body) + } +} + +// DeleteExceptionListItem deletes an existing exception list item. +func DeleteExceptionListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.DeleteExceptionListItemParams) diag.Diagnostics { + resp, err := client.API.DeleteExceptionListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params) + if err != nil { + return diagutil.FrameworkDiagFromError(err) + } + + switch resp.StatusCode() { + case http.StatusOK: + return nil + case http.StatusNotFound: + return nil + default: + return reportUnknownError(resp.StatusCode(), resp.Body) + } +} From ef69978c1d0d7071a78a481cb8d3f8ab6173293e Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 13:37:23 -0700 Subject: [PATCH 04/10] Use composite id for internal id --- internal/kibana/security_exception_list/create.go | 7 ++++++- internal/kibana/security_exception_list/delete.go | 14 +++++++++++--- internal/kibana/security_exception_list/read.go | 14 +++++++++++--- internal/kibana/security_exception_list/update.go | 10 +++++++++- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/internal/kibana/security_exception_list/create.go b/internal/kibana/security_exception_list/create.go index ff26fa6f4..200a76820 100644 --- a/internal/kibana/security_exception_list/create.go +++ b/internal/kibana/security_exception_list/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" "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" @@ -129,7 +130,11 @@ func (r *ExceptionListResource) Create(ctx context.Context, req resource.CreateR func (r *ExceptionListResource) updateStateFromAPIResponse(ctx context.Context, model *ExceptionListModel, apiResp *kbapi.SecurityExceptionsAPIExceptionList) diag.Diagnostics { var diags diag.Diagnostics - model.ID = types.StringValue(string(apiResp.Id)) + compId := clients.CompositeId{ + ClusterId: model.SpaceID.ValueString(), + ResourceId: string(apiResp.Id), + } + model.ID = types.StringValue(compId.String()) model.ListID = types.StringValue(string(apiResp.ListId)) model.Name = types.StringValue(string(apiResp.Name)) model.Description = types.StringValue(string(apiResp.Description)) diff --git a/internal/kibana/security_exception_list/delete.go b/internal/kibana/security_exception_list/delete.go index 0c2b34b96..1667e332b 100644 --- a/internal/kibana/security_exception_list/delete.go +++ b/internal/kibana/security_exception_list/delete.go @@ -4,6 +4,7 @@ import ( "context" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -23,12 +24,19 @@ func (r *ExceptionListResource) Delete(ctx context.Context, req resource.DeleteR return } - // Delete by ID - id := kbapi.SecurityExceptionsAPIExceptionListId(state.ID.ValueString()) + // Parse composite ID to get space_id and resource_id + compId, compIdDiags := clients.CompositeIdFromStrFw(state.ID.ValueString()) + resp.Diagnostics.Append(compIdDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Delete by resource ID from composite ID + id := kbapi.SecurityExceptionsAPIExceptionListId(compId.ResourceId) params := &kbapi.DeleteExceptionListParams{ Id: &id, } - diags = kibana_oapi.DeleteExceptionList(ctx, client, state.SpaceID.ValueString(), params) + diags = kibana_oapi.DeleteExceptionList(ctx, client, compId.ClusterId, params) resp.Diagnostics.Append(diags...) } diff --git a/internal/kibana/security_exception_list/read.go b/internal/kibana/security_exception_list/read.go index 1ace40cb5..60551b0c5 100644 --- a/internal/kibana/security_exception_list/read.go +++ b/internal/kibana/security_exception_list/read.go @@ -4,6 +4,7 @@ import ( "context" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -23,13 +24,20 @@ func (r *ExceptionListResource) Read(ctx context.Context, req resource.ReadReque return } - // Read by ID - id := kbapi.SecurityExceptionsAPIExceptionListId(state.ID.ValueString()) + // Parse composite ID to get space_id and resource_id + compId, compIdDiags := clients.CompositeIdFromStrFw(state.ID.ValueString()) + resp.Diagnostics.Append(compIdDiags...) + if resp.Diagnostics.HasError() { + return + } + + // Read by resource ID from composite ID + id := kbapi.SecurityExceptionsAPIExceptionListId(compId.ResourceId) params := &kbapi.ReadExceptionListParams{ Id: &id, } - readResp, diags := kibana_oapi.GetExceptionList(ctx, client, state.SpaceID.ValueString(), params) + readResp, diags := kibana_oapi.GetExceptionList(ctx, client, compId.ClusterId, params) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_exception_list/update.go b/internal/kibana/security_exception_list/update.go index 97abd0d5d..70172682a 100644 --- a/internal/kibana/security_exception_list/update.go +++ b/internal/kibana/security_exception_list/update.go @@ -5,6 +5,7 @@ import ( "encoding/json" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" + "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/elastic/terraform-provider-elasticstack/internal/utils" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -25,8 +26,15 @@ func (r *ExceptionListResource) Update(ctx context.Context, req resource.UpdateR return } + // Parse composite ID to get space_id and resource_id + compId, compIdDiags := clients.CompositeIdFromStrFw(plan.ID.ValueString()) + resp.Diagnostics.Append(compIdDiags...) + if resp.Diagnostics.HasError() { + return + } + // Build the update request body - id := kbapi.SecurityExceptionsAPIExceptionListId(plan.ID.ValueString()) + id := kbapi.SecurityExceptionsAPIExceptionListId(compId.ResourceId) body := kbapi.UpdateExceptionListJSONRequestBody{ Id: &id, Name: kbapi.SecurityExceptionsAPIExceptionListName(plan.Name.ValueString()), From 3980a354d2b39e74a73ee937002a364318ee4a53 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 15:23:32 -0700 Subject: [PATCH 05/10] Add plugin to provider --- provider/plugin_framework.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/provider/plugin_framework.go b/provider/plugin_framework.go index e6aa43a00..8868817e0 100644 --- a/provider/plugin_framework.go +++ b/provider/plugin_framework.go @@ -33,6 +33,7 @@ import ( "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/security_detection_rule" + "github.com/elastic/terraform-provider-elasticstack/internal/kibana/security_exception_list" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/security_list" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/spaces" "github.com/elastic/terraform-provider-elasticstack/internal/kibana/synthetics/monitor" @@ -150,6 +151,7 @@ func (p *Provider) resources(ctx context.Context) []func() resource.Resource { func (p *Provider) experimentalResources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ security_list.NewResource, + security_exception_list.NewResource, } } From 7b2f1d07b7bad5ed0de434e235ba7dc5812ed805 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 15:30:12 -0700 Subject: [PATCH 06/10] Set space from composite id / add import test --- .../security_exception_list/acc_test.go | 32 +++++++++++++++++++ .../kibana/security_exception_list/read.go | 5 +++ 2 files changed, 37 insertions(+) diff --git a/internal/kibana/security_exception_list/acc_test.go b/internal/kibana/security_exception_list/acc_test.go index 89059330d..c226261fb 100644 --- a/internal/kibana/security_exception_list/acc_test.go +++ b/internal/kibana/security_exception_list/acc_test.go @@ -61,6 +61,22 @@ func TestAccResourceExceptionList(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_security_exception_list.test", "tags.1", "updated"), ), }, + { // Import + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport), + ProtoV6ProviderFactories: acctest.Providers, + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "list_id": config.StringVariable("test-exception-list"), + "name": config.StringVariable("Test Exception List Updated"), + "description": config.StringVariable("Updated description"), + "type": config.StringVariable("detection"), + "namespace_type": config.StringVariable("single"), + "tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("updated")), + }, + ResourceName: "elasticstack_kibana_security_exception_list.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -131,6 +147,22 @@ func TestAccResourceExceptionListWithSpace(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "tags.2", "updated"), ), }, + { // Import + SkipFunc: versionutils.CheckIfVersionIsUnsupported(minExceptionListAPISupport), + ConfigDirectory: acctest.NamedTestCaseDirectory("update"), + ConfigVariables: config.Variables{ + "space_id": config.StringVariable(spaceID), + "list_id": config.StringVariable("test-exception-list-space"), + "name": config.StringVariable("Test Exception List in Space Updated"), + "description": config.StringVariable("Updated description in space"), + "type": config.StringVariable("detection"), + "namespace_type": config.StringVariable("single"), + "tags": config.ListVariable(config.StringVariable("test"), config.StringVariable("space"), config.StringVariable("updated")), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, }, }) } diff --git a/internal/kibana/security_exception_list/read.go b/internal/kibana/security_exception_list/read.go index 60551b0c5..d12223d36 100644 --- a/internal/kibana/security_exception_list/read.go +++ b/internal/kibana/security_exception_list/read.go @@ -7,6 +7,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" ) func (r *ExceptionListResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -31,6 +32,10 @@ func (r *ExceptionListResource) Read(ctx context.Context, req resource.ReadReque return } + if !compIdDiags.HasError() { + state.SpaceID = types.StringValue(compId.ClusterId) + } + // Read by resource ID from composite ID id := kbapi.SecurityExceptionsAPIExceptionListId(compId.ResourceId) params := &kbapi.ReadExceptionListParams{ From a19c9814e412a492ef97f8de635f6e89ad445e32 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Thu, 27 Nov 2025 15:50:11 -0700 Subject: [PATCH 07/10] Use normalized json type for "meta" --- .../kibana/security_exception_list/create.go | 10 +++--- .../kibana/security_exception_list/models.go | 33 ++++++++++--------- .../kibana/security_exception_list/schema.go | 2 ++ .../kibana/security_exception_list/update.go | 6 ++-- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/internal/kibana/security_exception_list/create.go b/internal/kibana/security_exception_list/create.go index 200a76820..39ad2772e 100644 --- a/internal/kibana/security_exception_list/create.go +++ b/internal/kibana/security_exception_list/create.go @@ -8,6 +8,7 @@ import ( "github.com/elastic/terraform-provider-elasticstack/internal/clients" "github.com/elastic/terraform-provider-elasticstack/internal/clients/kibana_oapi" "github.com/elastic/terraform-provider-elasticstack/internal/utils" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" @@ -76,8 +77,9 @@ func (r *ExceptionListResource) Create(ctx context.Context, req resource.CreateR // Set optional meta if utils.IsKnown(plan.Meta) { var meta kbapi.SecurityExceptionsAPIExceptionListMeta - if err := json.Unmarshal([]byte(plan.Meta.ValueString()), &meta); err != nil { - resp.Diagnostics.AddError("Failed to parse meta JSON", err.Error()) + unmarshalDiags := plan.Meta.Unmarshal(&meta) + resp.Diagnostics.Append(unmarshalDiags...) + if resp.Diagnostics.HasError() { return } body.Meta = &meta @@ -177,9 +179,9 @@ func (r *ExceptionListResource) updateStateFromAPIResponse(ctx context.Context, diags.AddError("Failed to serialize meta", err.Error()) return diags } - model.Meta = types.StringValue(string(metaJSON)) + model.Meta = jsontypes.NewNormalizedValue(string(metaJSON)) } else { - model.Meta = types.StringNull() + model.Meta = jsontypes.NewNormalizedNull() } return diags diff --git a/internal/kibana/security_exception_list/models.go b/internal/kibana/security_exception_list/models.go index 5e97aa9f9..c96f18214 100644 --- a/internal/kibana/security_exception_list/models.go +++ b/internal/kibana/security_exception_list/models.go @@ -1,24 +1,25 @@ package security_exception_list import ( + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework/types" ) type ExceptionListModel struct { - ID types.String `tfsdk:"id"` - SpaceID types.String `tfsdk:"space_id"` - ListID types.String `tfsdk:"list_id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Type types.String `tfsdk:"type"` - NamespaceType types.String `tfsdk:"namespace_type"` - OsTypes types.List `tfsdk:"os_types"` - Tags types.List `tfsdk:"tags"` - Meta types.String `tfsdk:"meta"` - CreatedAt types.String `tfsdk:"created_at"` - CreatedBy types.String `tfsdk:"created_by"` - UpdatedAt types.String `tfsdk:"updated_at"` - UpdatedBy types.String `tfsdk:"updated_by"` - Immutable types.Bool `tfsdk:"immutable"` - TieBreakerID types.String `tfsdk:"tie_breaker_id"` + ID types.String `tfsdk:"id"` + SpaceID types.String `tfsdk:"space_id"` + ListID types.String `tfsdk:"list_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Type types.String `tfsdk:"type"` + NamespaceType types.String `tfsdk:"namespace_type"` + OsTypes types.List `tfsdk:"os_types"` + Tags types.List `tfsdk:"tags"` + Meta jsontypes.Normalized `tfsdk:"meta"` + CreatedAt types.String `tfsdk:"created_at"` + CreatedBy types.String `tfsdk:"created_by"` + UpdatedAt types.String `tfsdk:"updated_at"` + UpdatedBy types.String `tfsdk:"updated_by"` + Immutable types.Bool `tfsdk:"immutable"` + TieBreakerID types.String `tfsdk:"tie_breaker_id"` } diff --git a/internal/kibana/security_exception_list/schema.go b/internal/kibana/security_exception_list/schema.go index 8d1f934a6..7e60ac2e2 100644 --- a/internal/kibana/security_exception_list/schema.go +++ b/internal/kibana/security_exception_list/schema.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" + "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -94,6 +95,7 @@ func (r *ExceptionListResource) Schema(_ context.Context, _ resource.SchemaReque "meta": schema.StringAttribute{ MarkdownDescription: "Placeholder for metadata about the list container as JSON string.", Optional: true, + CustomType: jsontypes.NormalizedType{}, }, "created_at": schema.StringAttribute{ MarkdownDescription: "The timestamp of when the exception list was created.", diff --git a/internal/kibana/security_exception_list/update.go b/internal/kibana/security_exception_list/update.go index 70172682a..f71cabd39 100644 --- a/internal/kibana/security_exception_list/update.go +++ b/internal/kibana/security_exception_list/update.go @@ -2,7 +2,6 @@ package security_exception_list import ( "context" - "encoding/json" "github.com/elastic/terraform-provider-elasticstack/generated/kbapi" "github.com/elastic/terraform-provider-elasticstack/internal/clients" @@ -84,8 +83,9 @@ func (r *ExceptionListResource) Update(ctx context.Context, req resource.UpdateR // Set optional meta if utils.IsKnown(plan.Meta) { var meta kbapi.SecurityExceptionsAPIExceptionListMeta - if err := json.Unmarshal([]byte(plan.Meta.ValueString()), &meta); err != nil { - resp.Diagnostics.AddError("Failed to parse meta JSON", err.Error()) + unmarshalDiags := plan.Meta.Unmarshal(&meta) + resp.Diagnostics.Append(unmarshalDiags...) + if resp.Diagnostics.HasError() { return } body.Meta = &meta From 1f7b81e2c9d4a5dcf8eb840da1d8a7f634b8cea6 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Fri, 28 Nov 2025 19:39:09 -0700 Subject: [PATCH 08/10] Use typeutils for casts --- internal/kibana/security_exception_list/create.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/kibana/security_exception_list/create.go b/internal/kibana/security_exception_list/create.go index 39ad2772e..6e3190e56 100644 --- a/internal/kibana/security_exception_list/create.go +++ b/internal/kibana/security_exception_list/create.go @@ -137,11 +137,11 @@ func (r *ExceptionListResource) updateStateFromAPIResponse(ctx context.Context, ResourceId: string(apiResp.Id), } model.ID = types.StringValue(compId.String()) - model.ListID = types.StringValue(string(apiResp.ListId)) - model.Name = types.StringValue(string(apiResp.Name)) - model.Description = types.StringValue(string(apiResp.Description)) - model.Type = types.StringValue(string(apiResp.Type)) - model.NamespaceType = types.StringValue(string(apiResp.NamespaceType)) + model.ListID = utils.StringishValue(apiResp.ListId) + model.Name = utils.StringishValue(apiResp.Name) + model.Description = utils.StringishValue(apiResp.Description) + model.Type = utils.StringishValue(apiResp.Type) + model.NamespaceType = utils.StringishValue(apiResp.NamespaceType) model.CreatedAt = types.StringValue(apiResp.CreatedAt.Format("2006-01-02T15:04:05.000Z")) model.CreatedBy = types.StringValue(apiResp.CreatedBy) model.UpdatedAt = types.StringValue(apiResp.UpdatedAt.Format("2006-01-02T15:04:05.000Z")) From 3e2d841f9718cc7e3f76ab2bf452b1170f8bce3a Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Fri, 28 Nov 2025 19:59:08 -0700 Subject: [PATCH 09/10] Return parsed response from exceptions.go --- internal/clients/kibana_oapi/exceptions.go | 24 +++++++++---------- .../kibana/security_exception_list/create.go | 8 +++---- .../kibana/security_exception_list/read.go | 4 ++-- .../kibana/security_exception_list/update.go | 8 +++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/internal/clients/kibana_oapi/exceptions.go b/internal/clients/kibana_oapi/exceptions.go index 8c1d39464..1f83b860e 100644 --- a/internal/clients/kibana_oapi/exceptions.go +++ b/internal/clients/kibana_oapi/exceptions.go @@ -10,7 +10,7 @@ import ( ) // GetExceptionList reads an exception list from the API by ID or list_id -func GetExceptionList(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadExceptionListParams) (*kbapi.ReadExceptionListResponse, diag.Diagnostics) { +func GetExceptionList(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadExceptionListParams) (*kbapi.SecurityExceptionsAPIExceptionList, diag.Diagnostics) { resp, err := client.API.ReadExceptionListWithResponse(ctx, kbapi.SpaceId(spaceId), params) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -18,7 +18,7 @@ func GetExceptionList(ctx context.Context, client *Client, spaceId string, param switch resp.StatusCode() { case http.StatusOK: - return resp, nil + return resp.JSON200, nil case http.StatusNotFound: return nil, nil default: @@ -27,7 +27,7 @@ func GetExceptionList(ctx context.Context, client *Client, spaceId string, param } // CreateExceptionList creates a new exception list. -func CreateExceptionList(ctx context.Context, client *Client, spaceId string, body kbapi.CreateExceptionListJSONRequestBody) (*kbapi.CreateExceptionListResponse, diag.Diagnostics) { +func CreateExceptionList(ctx context.Context, client *Client, spaceId string, body kbapi.CreateExceptionListJSONRequestBody) (*kbapi.SecurityExceptionsAPIExceptionList, diag.Diagnostics) { resp, err := client.API.CreateExceptionListWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -35,14 +35,14 @@ func CreateExceptionList(ctx context.Context, client *Client, spaceId string, bo switch resp.StatusCode() { case http.StatusOK: - return resp, nil + return resp.JSON200, nil default: return nil, reportUnknownError(resp.StatusCode(), resp.Body) } } // UpdateExceptionList updates an existing exception list. -func UpdateExceptionList(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateExceptionListJSONRequestBody) (*kbapi.UpdateExceptionListResponse, diag.Diagnostics) { +func UpdateExceptionList(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateExceptionListJSONRequestBody) (*kbapi.SecurityExceptionsAPIExceptionList, diag.Diagnostics) { resp, err := client.API.UpdateExceptionListWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -50,7 +50,7 @@ func UpdateExceptionList(ctx context.Context, client *Client, spaceId string, bo switch resp.StatusCode() { case http.StatusOK: - return resp, nil + return resp.JSON200, nil default: return nil, reportUnknownError(resp.StatusCode(), resp.Body) } @@ -74,7 +74,7 @@ func DeleteExceptionList(ctx context.Context, client *Client, spaceId string, pa } // GetExceptionListItem reads an exception list item from the API by ID or item_id -func GetExceptionListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadExceptionListItemParams) (*kbapi.ReadExceptionListItemResponse, diag.Diagnostics) { +func GetExceptionListItem(ctx context.Context, client *Client, spaceId string, params *kbapi.ReadExceptionListItemParams) (*kbapi.SecurityExceptionsAPIExceptionListItem, diag.Diagnostics) { resp, err := client.API.ReadExceptionListItemWithResponse(ctx, kbapi.SpaceId(spaceId), params) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -82,7 +82,7 @@ func GetExceptionListItem(ctx context.Context, client *Client, spaceId string, p switch resp.StatusCode() { case http.StatusOK: - return resp, nil + return resp.JSON200, nil case http.StatusNotFound: return nil, nil default: @@ -91,7 +91,7 @@ func GetExceptionListItem(ctx context.Context, client *Client, spaceId string, p } // CreateExceptionListItem creates a new exception list item. -func CreateExceptionListItem(ctx context.Context, client *Client, spaceId string, body kbapi.CreateExceptionListItemJSONRequestBody) (*kbapi.CreateExceptionListItemResponse, diag.Diagnostics) { +func CreateExceptionListItem(ctx context.Context, client *Client, spaceId string, body kbapi.CreateExceptionListItemJSONRequestBody) (*kbapi.SecurityExceptionsAPIExceptionListItem, diag.Diagnostics) { resp, err := client.API.CreateExceptionListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -99,14 +99,14 @@ func CreateExceptionListItem(ctx context.Context, client *Client, spaceId string switch resp.StatusCode() { case http.StatusOK: - return resp, nil + return resp.JSON200, nil default: return nil, reportUnknownError(resp.StatusCode(), resp.Body) } } // UpdateExceptionListItem updates an existing exception list item. -func UpdateExceptionListItem(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateExceptionListItemJSONRequestBody) (*kbapi.UpdateExceptionListItemResponse, diag.Diagnostics) { +func UpdateExceptionListItem(ctx context.Context, client *Client, spaceId string, body kbapi.UpdateExceptionListItemJSONRequestBody) (*kbapi.SecurityExceptionsAPIExceptionListItem, diag.Diagnostics) { resp, err := client.API.UpdateExceptionListItemWithResponse(ctx, kbapi.SpaceId(spaceId), body) if err != nil { return nil, diagutil.FrameworkDiagFromError(err) @@ -114,7 +114,7 @@ func UpdateExceptionListItem(ctx context.Context, client *Client, spaceId string switch resp.StatusCode() { case http.StatusOK: - return resp, nil + return resp.JSON200, nil default: return nil, reportUnknownError(resp.StatusCode(), resp.Body) } diff --git a/internal/kibana/security_exception_list/create.go b/internal/kibana/security_exception_list/create.go index 6e3190e56..a0e3f9325 100644 --- a/internal/kibana/security_exception_list/create.go +++ b/internal/kibana/security_exception_list/create.go @@ -92,7 +92,7 @@ func (r *ExceptionListResource) Create(ctx context.Context, req resource.CreateR return } - if createResp == nil || createResp.JSON200 == nil { + if createResp == nil { resp.Diagnostics.AddError("Failed to create exception list", "API returned empty response") return } @@ -103,7 +103,7 @@ func (r *ExceptionListResource) Create(ctx context.Context, req resource.CreateR */ // Read back the created resource to get the final state readParams := &kbapi.ReadExceptionListParams{ - Id: (*kbapi.SecurityExceptionsAPIExceptionListId)(&createResp.JSON200.Id), + Id: (*kbapi.SecurityExceptionsAPIExceptionListId)(&createResp.Id), } readResp, diags := kibana_oapi.GetExceptionList(ctx, client, plan.SpaceID.ValueString(), readParams) @@ -112,14 +112,14 @@ func (r *ExceptionListResource) Create(ctx context.Context, req resource.CreateR return } - if readResp == nil || readResp.JSON200 == nil { + if readResp == nil { resp.State.RemoveResource(ctx) resp.Diagnostics.AddError("Failed to fetch exception list", "API returned empty response") return } // Update state with read response - diags = r.updateStateFromAPIResponse(ctx, &plan, readResp.JSON200) + diags = r.updateStateFromAPIResponse(ctx, &plan, readResp) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_exception_list/read.go b/internal/kibana/security_exception_list/read.go index d12223d36..82e92474f 100644 --- a/internal/kibana/security_exception_list/read.go +++ b/internal/kibana/security_exception_list/read.go @@ -48,13 +48,13 @@ func (r *ExceptionListResource) Read(ctx context.Context, req resource.ReadReque return } - if readResp == nil || readResp.JSON200 == nil { + if readResp == nil { resp.State.RemoveResource(ctx) return } // Update state with response - diags = r.updateStateFromAPIResponse(ctx, &state, readResp.JSON200) + diags = r.updateStateFromAPIResponse(ctx, &state, readResp) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/internal/kibana/security_exception_list/update.go b/internal/kibana/security_exception_list/update.go index f71cabd39..b1723c31a 100644 --- a/internal/kibana/security_exception_list/update.go +++ b/internal/kibana/security_exception_list/update.go @@ -98,7 +98,7 @@ func (r *ExceptionListResource) Update(ctx context.Context, req resource.UpdateR return } - if updateResp == nil || updateResp.JSON200 == nil { + if updateResp == nil { resp.Diagnostics.AddError("Failed to update exception list", "API returned empty response") return } @@ -109,7 +109,7 @@ func (r *ExceptionListResource) Update(ctx context.Context, req resource.UpdateR */ // Read back the updated resource to get the final state readParams := &kbapi.ReadExceptionListParams{ - Id: (*kbapi.SecurityExceptionsAPIExceptionListId)(&updateResp.JSON200.Id), + Id: (*kbapi.SecurityExceptionsAPIExceptionListId)(&updateResp.Id), } readResp, diags := kibana_oapi.GetExceptionList(ctx, client, plan.SpaceID.ValueString(), readParams) @@ -118,14 +118,14 @@ func (r *ExceptionListResource) Update(ctx context.Context, req resource.UpdateR return } - if readResp == nil || readResp.JSON200 == nil { + if readResp == nil { resp.State.RemoveResource(ctx) resp.Diagnostics.AddError("Failed to fetch exception list", "API returned empty response") return } // Update state with read response - diags = r.updateStateFromAPIResponse(ctx, &plan, readResp.JSON200) + diags = r.updateStateFromAPIResponse(ctx, &plan, readResp) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From 67d43e1f9c75002771e0401938148e648ecd4af4 Mon Sep 17 00:00:00 2001 From: Nick Benoit Date: Fri, 28 Nov 2025 20:13:19 -0700 Subject: [PATCH 10/10] Extract valid list types into a variable --- .../kibana/security_exception_list/schema.go | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/internal/kibana/security_exception_list/schema.go b/internal/kibana/security_exception_list/schema.go index 7e60ac2e2..200b1c277 100644 --- a/internal/kibana/security_exception_list/schema.go +++ b/internal/kibana/security_exception_list/schema.go @@ -3,6 +3,7 @@ package security_exception_list import ( "context" _ "embed" + "strings" "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -18,6 +19,15 @@ import ( //go:embed resource-description.md var exceptionListResourceDescription string +var validExceptionListTypes = []string{ + "detection", + "endpoint", + "endpoint_trusted_apps", + "endpoint_events", + "endpoint_host_isolation_exceptions", + "endpoint_blocklists", +} + func (r *ExceptionListResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: exceptionListResourceDescription, @@ -54,16 +64,11 @@ func (r *ExceptionListResource) Schema(_ context.Context, _ resource.SchemaReque Required: true, }, "type": schema.StringAttribute{ - MarkdownDescription: "The type of exception list. Can be one of: `detection`, `endpoint`, `endpoint_trusted_apps`, `endpoint_events`, `endpoint_host_isolation_exceptions`, `endpoint_blocklists`.", + MarkdownDescription: "The type of exception list. Can be one of: " + strings.Join(wrapInBackticks(validExceptionListTypes), ", ") + ".", Required: true, Validators: []validator.String{ stringvalidator.OneOf( - "detection", - "endpoint", - "endpoint_trusted_apps", - "endpoint_events", - "endpoint_host_isolation_exceptions", - "endpoint_blocklists", + validExceptionListTypes..., ), }, PlanModifiers: []planmodifier.String{ @@ -124,3 +129,11 @@ func (r *ExceptionListResource) Schema(_ context.Context, _ resource.SchemaReque }, } } + +func wrapInBackticks(strs []string) []string { + result := make([]string, len(strs)) + for i, s := range strs { + result[i] = "`" + s + "`" + } + return result +}