From 51b786e385881b9d9327f979904726536262e7a7 Mon Sep 17 00:00:00 2001 From: ffalor <35144141+ffalor@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:08:38 -0500 Subject: [PATCH] allow `crowdstrike_sensor_update_policy` to assign host groups --- .github/workflows/ci.yml | 1 + docs/resources/sensor_update_policy.md | 2 + .../resource.tf | 1 + internal/provider/host_group_resource_test.go | 1 + internal/provider/provider_test.go | 12 ++ ...r_update_policy_builds_data_source_test.go | 1 + .../provider/sensor_update_policy_resource.go | 195 +++++++++++++++++- .../sensor_update_policy_resourse_test.go | 142 +++++++++++++ 8 files changed, 349 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f30f73..9c02eb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,5 +90,6 @@ jobs: TF_ACC: "1" FALCON_CLIENT_ID: ${{ secrets.FALCON_CLIENT_ID }} FALCON_CLIENT_SECRET: ${{ secrets.FALCON_CLIENT_SECRET }} + HOST_GROUP_ID: ${{ secrets.HOST_GROUP_ID }} run: go test -v -cover ./internal/provider/ diff --git a/docs/resources/sensor_update_policy.md b/docs/resources/sensor_update_policy.md index d14c693..99e9370 100644 --- a/docs/resources/sensor_update_policy.md +++ b/docs/resources/sensor_update_policy.md @@ -33,6 +33,7 @@ resource "crowdstrike_sensor_update_policy" "test" { platform_name = "Windows" build = "18110" uninstall_protection = false + # host_groups = ["host_group_id"] } output "sensor_policy" { @@ -53,6 +54,7 @@ output "sensor_policy" { - `description` (String) Sensor Update Policy description - `enabled` (Boolean) Enable the Sensor Update Policy +- `host_groups` (List of String) Host Group ids to attach to the policy - `uninstall_protection` (Boolean) Enable uninstall protection ### Read-Only diff --git a/examples/resources/crowdstrike_sensor_update_policy/resource.tf b/examples/resources/crowdstrike_sensor_update_policy/resource.tf index 9b94ebd..94336fb 100644 --- a/examples/resources/crowdstrike_sensor_update_policy/resource.tf +++ b/examples/resources/crowdstrike_sensor_update_policy/resource.tf @@ -18,6 +18,7 @@ resource "crowdstrike_sensor_update_policy" "test" { platform_name = "Windows" build = "18110" uninstall_protection = false + # host_groups = ["host_group_id"] } output "sensor_policy" { diff --git a/internal/provider/host_group_resource_test.go b/internal/provider/host_group_resource_test.go index 7657142..d7bca08 100644 --- a/internal/provider/host_group_resource_test.go +++ b/internal/provider/host_group_resource_test.go @@ -12,6 +12,7 @@ func TestAccHostGroupResource(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acceptance-test") resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ // Create and Read testing { diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index e2b9ad7..9c62714 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,6 +1,9 @@ package provider import ( + "os" + "testing" + "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) @@ -18,3 +21,12 @@ provider "crowdstrike" {} var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ "crowdstrike": providerserver.NewProtocol6WithError(New("test")()), } + +func testAccPreCheck(t *testing.T) { + requiredEnvVars := []string{"FALCON_CLIENT_ID", "FALCON_CLIENT_SECRET", "HOST_GROUP_ID"} + for _, envVar := range requiredEnvVars { + if v := os.Getenv(envVar); v == "" { + t.Fatalf("%s must be set for acceptance tests", envVar) + } + } +} diff --git a/internal/provider/sensor_update_policy_builds_data_source_test.go b/internal/provider/sensor_update_policy_builds_data_source_test.go index 84e1e1b..68d8b18 100644 --- a/internal/provider/sensor_update_policy_builds_data_source_test.go +++ b/internal/provider/sensor_update_policy_builds_data_source_test.go @@ -9,6 +9,7 @@ import ( func TestAccSensorUpdatePolicyBuildsDataSource(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ // Read testing { diff --git a/internal/provider/sensor_update_policy_resource.go b/internal/provider/sensor_update_policy_resource.go index e559189..b700c8b 100644 --- a/internal/provider/sensor_update_policy_resource.go +++ b/internal/provider/sensor_update_policy_resource.go @@ -3,12 +3,14 @@ package provider import ( "context" "fmt" + "strings" "time" "github.com/crowdstrike/gofalcon/falcon/client" "github.com/crowdstrike/gofalcon/falcon/client/sensor_update_policies" "github.com/crowdstrike/gofalcon/falcon/models" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -26,6 +28,17 @@ var ( _ resource.ResourceWithImportState = &sensorUpdatePolicyResource{} ) +type hostGroupAction int + +const ( + removeHostGroup hostGroupAction = iota + addHostGroup +) + +func (h hostGroupAction) String() string { + return [...]string{"remove-host-group", "add-host-group"}[h] +} + // NewSensorUpdatePolicyResource is a helper function to simplify the provider implementation. func NewSensorUpdatePolicyResource() resource.Resource { return &sensorUpdatePolicyResource{} @@ -46,6 +59,7 @@ type sensorUpdatePolicyResourceModel struct { PlatformName types.String `tfsdk:"platform_name"` UninstallProtection types.Bool `tfsdk:"uninstall_protection"` LastUpdated types.String `tfsdk:"last_updated"` + HostGroups types.List `tfsdk:"host_groups"` } // Configure adds the provider configured client to the resource. @@ -133,6 +147,11 @@ func (r *sensorUpdatePolicyResource) Schema( Description: "Enable uninstall protection", Default: booldefault.StaticBool(false), }, + "host_groups": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + Description: "Host Group ids to attach to the policy", + }, }, } } @@ -151,16 +170,13 @@ func (r *sensorUpdatePolicyResource) Create( return } - policyName := plan.Name.ValueString() - platformName := plan.PlatformName.ValueString() - policyParams := sensor_update_policies.CreateSensorUpdatePoliciesV2Params{ Context: ctx, Body: &models.SensorUpdateCreatePoliciesReqV2{ Resources: []*models.SensorUpdateCreatePolicyReqV2{ { - Name: &policyName, - PlatformName: &platformName, + Name: plan.Name.ValueStringPointer(), + PlatformName: plan.PlatformName.ValueStringPointer(), Description: plan.Description.ValueString(), Settings: &models.SensorUpdateSettingsReqV2{ Build: plan.Build.ValueString(), @@ -176,7 +192,6 @@ func (r *sensorUpdatePolicyResource) Create( } else { uninstallProtection = "DISABLED" } - policyParams.Body.Resources[0].Settings.UninstallProtection = uninstallProtection policy, err := r.client.SensorUpdatePolicies.CreateSensorUpdatePoliciesV2(&policyParams) @@ -211,6 +226,24 @@ func (r *sensorUpdatePolicyResource) Create( plan.Enabled = types.BoolValue(*actionResp.Payload.Resources[0].Enabled) } + if len(plan.HostGroups.Elements()) != 0 { + var hostGroupIDs []string + resp.Diagnostics.Append(plan.HostGroups.ElementsAs(ctx, &hostGroupIDs, false)...) + if resp.Diagnostics.HasError() { + return + } + + err = r.updateHostGroups(ctx, addHostGroup, hostGroupIDs, plan.ID.ValueString()) + + if err != nil { + resp.Diagnostics.AddError( + "Error assinging host group to policy", + "Could not assign host group to policy, unexpected error: "+err.Error(), + ) + return + } + } + diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -254,12 +287,26 @@ func (r *sensorUpdatePolicyResource) Read( state.Build = types.StringValue(*policyResource.Settings.Build) state.PlatformName = types.StringValue(*policyResource.PlatformName) state.Enabled = types.BoolValue(*policyResource.Enabled) + if *policyResource.Settings.UninstallProtection == "ENABLED" { state.UninstallProtection = types.BoolValue(true) } else { state.UninstallProtection = types.BoolValue(false) } + var hostGroups []string + for _, hostGroup := range policyResource.Groups { + hostGroups = append(hostGroups, *hostGroup.ID) + } + + hostGroupIDs, diags := types.ListValueFrom(ctx, types.StringType, hostGroups) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + state.HostGroups = hostGroupIDs + // Set refreshed state diags = resp.State.Set(ctx, &state) resp.Diagnostics.Append(diags...) @@ -281,6 +328,51 @@ func (r *sensorUpdatePolicyResource) Update( if resp.Diagnostics.HasError() { return } + // Retrieve values from state + var state sensorUpdatePolicyResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + hostGroupsToAdd, hostGroupsToRemove, diags := r.getHostGroupsToModify(ctx, plan, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if len(hostGroupsToAdd) != 0 { + err := r.updateHostGroups(ctx, addHostGroup, hostGroupsToAdd, plan.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error updating CrowdStrike sensor update policy", + fmt.Sprintf( + "Could not add host groups: (%s) to policy with id: %s \n\n %s", + strings.Join(hostGroupsToAdd, ", "), + plan.ID.ValueString(), + err.Error(), + ), + ) + return + } + } + + if len(hostGroupsToRemove) != 0 { + err := r.updateHostGroups(ctx, removeHostGroup, hostGroupsToRemove, plan.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error updating CrowdStrike sensor update policy", + fmt.Sprintf( + "Could not remove host groups: (%s) to policy with id: %s \n\n %s", + strings.Join(hostGroupsToAdd, ", "), + plan.ID.ValueString(), + err.Error(), + ), + ) + return + } + } policyParams := sensor_update_policies.UpdateSensorUpdatePoliciesV2Params{ Context: ctx, @@ -345,6 +437,19 @@ func (r *sensorUpdatePolicyResource) Update( plan.Enabled = types.BoolValue(*actionResp.Payload.Resources[0].Enabled) + var hostGroups []string + for _, hostGroup := range policyResource.Groups { + hostGroups = append(hostGroups, *hostGroup.ID) + } + + hostGroupIDs, diags := types.ListValueFrom(ctx, types.StringType, hostGroups) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + plan.HostGroups = hostGroupIDs + diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -430,3 +535,81 @@ func (r *sensorUpdatePolicyResource) updatePolicyEnabledState( return *res, err } + +// updateHostGroups will remove or add a slice of host groups +// to a slice of sensor update policies. +func (r *sensorUpdatePolicyResource) updateHostGroups( + ctx context.Context, + action hostGroupAction, + hostGroupIDs []string, + policyID string, +) error { + var actionParams []*models.MsaspecActionParameter + name := "group_id" + + for _, hostGroup := range hostGroupIDs { + actionParam := &models.MsaspecActionParameter{ + Name: &name, + Value: &hostGroup, + } + + actionParams = append(actionParams, actionParam) + } + + _, err := r.client.SensorUpdatePolicies.PerformSensorUpdatePoliciesAction( + &sensor_update_policies.PerformSensorUpdatePoliciesActionParams{ + Context: ctx, + ActionName: action.String(), + Body: &models.MsaEntityActionRequestV2{ + ActionParameters: actionParams, + Ids: []string{policyID}, + }, + }, + ) + + return err +} + +// getHostGroupsToModify takes in the planned state and current state and returns +// a list of host group ids to remove and add. +func (r *sensorUpdatePolicyResource) getHostGroupsToModify( + ctx context.Context, + plan, state sensorUpdatePolicyResourceModel, +) (hostGroupsToAdd []string, hostGroupsToRemove []string, diags diag.Diagnostics) { + var planHostGroupIDs, stateHostGroupIds []string + planMap := make(map[string]bool) + stateMap := make(map[string]bool) + + d := plan.HostGroups.ElementsAs(ctx, &planHostGroupIDs, false) + diags.Append(d...) + if diags.HasError() { + return + } + d = state.HostGroups.ElementsAs(ctx, &stateHostGroupIds, false) + diags.Append(d...) + if diags.HasError() { + return + } + + for _, id := range planHostGroupIDs { + planMap[id] = true + } + + for _, id := range stateHostGroupIds { + stateMap[id] = true + } + + for _, id := range planHostGroupIDs { + if !stateMap[id] { + hostGroupsToAdd = append(hostGroupsToAdd, id) + } + } + + for _, id := range stateHostGroupIds { + if !planMap[id] { + hostGroupsToRemove = append(hostGroupsToRemove, id) + } + } + + return +} diff --git a/internal/provider/sensor_update_policy_resourse_test.go b/internal/provider/sensor_update_policy_resourse_test.go index b4faaaa..cf69979 100644 --- a/internal/provider/sensor_update_policy_resourse_test.go +++ b/internal/provider/sensor_update_policy_resourse_test.go @@ -2,6 +2,7 @@ package provider import ( "fmt" + "os" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -12,6 +13,7 @@ func TestAccSensorUpdatePolicyResource(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acceptance-test") resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ // Create and Read testing { @@ -132,3 +134,143 @@ resource "crowdstrike_sensor_update_policy" "test" { }, }) } + +func TestAccSensorUpdatePolicyResourceWithHostGroup(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acceptance-test") + hostGroupID, _ := os.LookupEnv("HOST_GROUP_ID") + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: providerConfig + fmt.Sprintf(` +resource "crowdstrike_sensor_update_policy" "test" { + name = "%s" + enabled = true + host_groups = ["%s"] + description = "made with terraform" + platform_name = "Windows" + build = "18110" + uninstall_protection = false +} +`, rName, hostGroupID), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "name", + rName, + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "description", + "made with terraform", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "enabled", + "true", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "platform_name", + "Windows", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "build", + "18110", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "uninstall_protection", + "false", + ), + resource.TestCheckResourceAttr("crowdstrike_sensor_update_policy.test", + "host_groups.#", + "1", + ), + resource.TestCheckResourceAttr("crowdstrike_sensor_update_policy.test", + "host_groups.0", + hostGroupID, + ), + // Verify dynamic values have any value set in the state. + resource.TestCheckResourceAttrSet( + "crowdstrike_sensor_update_policy.test", + "id", + ), + resource.TestCheckResourceAttrSet( + "crowdstrike_sensor_update_policy.test", + "last_updated", + ), + ), + }, + // ImportState testing + { + ResourceName: "crowdstrike_sensor_update_policy.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"last_updated"}, + }, + // Update and Read testing + { + Config: providerConfig + fmt.Sprintf(` +resource "crowdstrike_sensor_update_policy" "test" { + name = "%s-updated" + enabled = false + description = "made with terraform updated" + platform_name = "Windows" + build = "18110" + uninstall_protection = true +} +`, rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "name", + rName+"-updated", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "description", + "made with terraform updated", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "enabled", + "false", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "platform_name", + "Windows", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "build", + "18110", + ), + resource.TestCheckResourceAttr( + "crowdstrike_sensor_update_policy.test", + "uninstall_protection", + "true", + ), + resource.TestCheckNoResourceAttr( + "crowdstrike_sensor_update_policy.test", + "host_groups", + ), + // Verify dynamic values have any value set in the state. + resource.TestCheckResourceAttrSet( + "crowdstrike_sensor_update_policy.test", + "id", + ), + resource.TestCheckResourceAttrSet( + "crowdstrike_sensor_update_policy.test", + "last_updated", + ), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +}