diff --git a/azurerm/internal/services/iothub/iothub_resource.go b/azurerm/internal/services/iothub/iothub_resource.go index 3971dd487e06..3c74441e2358 100644 --- a/azurerm/internal/services/iothub/iothub_resource.go +++ b/azurerm/internal/services/iothub/iothub_resource.go @@ -602,12 +602,23 @@ func resourceIotHubCreateUpdate(d *schema.ResourceData, meta interface{}) error props.Properties.MinTLSVersion = utils.String(v.(string)) } - future, err := client.CreateOrUpdate(ctx, resourceGroup, name, props, "") + _, err = client.CreateOrUpdate(ctx, resourceGroup, name, props, "") if err != nil { return fmt.Errorf("Error creating/updating IotHub %q (Resource Group %q): %+v", name, resourceGroup, err) } - if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + timeout := schema.TimeoutUpdate + if d.IsNewResource() { + timeout = schema.TimeoutCreate + } + stateConf := &resource.StateChangeConf{ + Pending: []string{"Activating", "Transitioning"}, + Target: []string{"Succeeded"}, + Refresh: iothubStateRefreshFunc(ctx, client, resourceGroup, name), + Timeout: d.Timeout(timeout), + } + + if _, err := stateConf.WaitForState(); err != nil { return fmt.Errorf("Error waiting for the completion of the creating/updating of IotHub %q (Resource Group %q): %+v", name, resourceGroup, err) } @@ -642,16 +653,13 @@ func resourceIotHubRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("retrieving IotHub Client %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } - keysResp, err := client.ListKeys(ctx, id.ResourceGroup, id.Name) - if err != nil { - return fmt.Errorf("listing keys for IoTHub %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) - } - - keyList := keysResp.Response() - keys := flattenIoTHubSharedAccessPolicy(keyList.Value) + if keysResp, err := client.ListKeys(ctx, id.ResourceGroup, id.Name); err == nil { + keyList := keysResp.Response() + keys := flattenIoTHubSharedAccessPolicy(keyList.Value) - if err := d.Set("shared_access_policy", keys); err != nil { - return fmt.Errorf("setting `shared_access_policy` in IoTHub %q: %+v", id.Name, err) + if err := d.Set("shared_access_policy", keys); err != nil { + return fmt.Errorf("setting `shared_access_policy` in IoTHub %q: %+v", id.Name, err) + } } if properties := hub.Properties; properties != nil { @@ -736,6 +744,20 @@ func resourceIotHubDelete(d *schema.ResourceData, meta interface{}) error { locks.ByName(id.Name, IothubResourceName) defer locks.UnlockByName(id.Name, IothubResourceName) + // when running acctest of `azurerm_iot_security_solution`, we found after delete the iot security solution, the iothub provisionState is `Transitioning` + // if we delete directly, the func `client.Delete` will throw error + // so first wait for the iotHub state become succeed + stateConf := &resource.StateChangeConf{ + Pending: []string{"Activating", "Transitioning"}, + Target: []string{"Succeeded"}, + Refresh: iothubStateRefreshFunc(ctx, client, id.ResourceGroup, id.Name), + Timeout: d.Timeout(schema.TimeoutDelete), + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("waiting for ProvisioningState of IotHub %q (Resource Group %q) to become `Succeeded`: %+v", id.Name, id.ResourceGroup, err) + } + future, err := client.Delete(ctx, id.ResourceGroup, id.Name) if err != nil { if response.WasNotFound(future.Response()) { @@ -764,6 +786,27 @@ func waitForIotHubToBeDeleted(ctx context.Context, client *devices.IotHubResourc return nil } +func iothubStateRefreshFunc(ctx context.Context, client *devices.IotHubResourceClient, resourceGroup, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.Get(ctx, resourceGroup, name) + + log.Printf("Retrieving IoTHub %q (Resource Group %q) returned Status %d", resourceGroup, name, res.StatusCode) + + if err != nil { + if utils.ResponseWasNotFound(res.Response) { + return res, "NotFound", nil + } + return nil, "", fmt.Errorf("polling for the Provisioning State of the IotHub %q (RG: %q): %+v", name, resourceGroup, err) + } + + if res.Properties == nil || res.Properties.ProvisioningState == nil { + return res, "", fmt.Errorf("polling for the Provisioning State of the IotHub %q (RG: %q): %+v", name, resourceGroup, err) + } + + return res, *res.Properties.ProvisioningState, nil + } +} + func iothubStateStatusCodeRefreshFunc(ctx context.Context, client *devices.IotHubResourceClient, resourceGroup, name string) resource.StateRefreshFunc { return func() (interface{}, string, error) { res, err := client.Get(ctx, resourceGroup, name) diff --git a/azurerm/internal/services/securitycenter/client/client.go b/azurerm/internal/services/securitycenter/client/client.go index cd35391ddcf4..cc4c19cf6601 100644 --- a/azurerm/internal/services/securitycenter/client/client.go +++ b/azurerm/internal/services/securitycenter/client/client.go @@ -7,6 +7,7 @@ import ( type Client struct { ContactsClient *security.ContactsClient + IotSecuritySolutionClient *security.IotSecuritySolutionClient PricingClient *security.PricingsClient WorkspaceClient *security.WorkspaceSettingsClient AdvancedThreatProtectionClient *security.AdvancedThreatProtectionClient @@ -21,6 +22,9 @@ func NewClient(o *common.ClientOptions) *Client { ContactsClient := security.NewContactsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation) o.ConfigureClient(&ContactsClient.Client, o.ResourceManagerAuthorizer) + IotSecuritySolutionClient := security.NewIotSecuritySolutionClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation) + o.ConfigureClient(&IotSecuritySolutionClient.Client, o.ResourceManagerAuthorizer) + PricingClient := security.NewPricingsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation) o.ConfigureClient(&PricingClient.Client, o.ResourceManagerAuthorizer) @@ -41,6 +45,7 @@ func NewClient(o *common.ClientOptions) *Client { return &Client{ ContactsClient: &ContactsClient, + IotSecuritySolutionClient: &IotSecuritySolutionClient, PricingClient: &PricingClient, WorkspaceClient: &WorkspaceClient, AdvancedThreatProtectionClient: &AdvancedThreatProtectionClient, diff --git a/azurerm/internal/services/securitycenter/iot_security_solution_resource.go b/azurerm/internal/services/securitycenter/iot_security_solution_resource.go new file mode 100644 index 000000000000..4d34abdd0502 --- /dev/null +++ b/azurerm/internal/services/securitycenter/iot_security_solution_resource.go @@ -0,0 +1,437 @@ +package securitycenter + +import ( + "fmt" + "log" + "time" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/suppress" + + "github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v3.0/security" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/location" + iothubValidate "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/iothub/validate" + loganalyticsValidate "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/loganalytics/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/securitycenter/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/securitycenter/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/set" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceIotSecuritySolution() *schema.Resource { + return &schema.Resource{ + Create: resourceIotSecuritySolutionCreateUpdate, + Read: resourceIotSecuritySolutionRead, + Update: resourceIotSecuritySolutionCreateUpdate, + Delete: resourceIotSecuritySolutionDelete, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.IotSecuritySolutionID(id) + return err + }), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.IotSecuritySolutionName, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "location": location.Schema(), + + "display_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "iothub_ids": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: iothubValidate.IotHubID, + }, + Set: set.HashStringIgnoreCase, + }, + + "log_analytics_workspace_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: loganalyticsValidate.LogAnalyticsWorkspaceID, + DiffSuppressFunc: suppress.CaseDifference, + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "log_unmasked_ips_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "events_to_export": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + string(security.RawEvents), + }, false), + }, + }, + + "recommendations_enabled": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "acr_authentication": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "agent_send_unutilized_msg": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "baseline": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "edge_hub_mem_optimize": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "edge_logging_option": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "inconsistent_module_settings": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "install_agent": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "ip_filter_deny_all": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "ip_filter_permissive_rule": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "open_ports": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "permissive_firewall_policy": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "permissive_input_firewall_rules": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "permissive_output_firewall_rules": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "privileged_docker_options": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "shared_credentials": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "vulnerable_tls_cipher_suite": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + }, + }, + + "query_for_resources": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "query_subscription_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.IsUUID, + }, + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceIotSecuritySolutionCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).SecurityCenter.IotSecuritySolutionClient + subscriptionId := meta.(*clients.Client).Account.SubscriptionId + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + location := location.Normalize(d.Get("location").(string)) + + resourceId := parse.NewIotSecuritySolutionID(subscriptionId, resourceGroup, name).ID() + if d.IsNewResource() { + existing, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for presence of existing Security Center Iot Security Solution %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + if !utils.ResponseWasNotFound(existing.Response) { + return tf.ImportAsExistsError("azurerm_iot_security_solution", resourceId) + } + } + + status := security.SolutionStatusDisabled + if d.Get("enabled").(bool) { + status = security.SolutionStatusEnabled + } + + unmaskedIPLoggingStatus := security.UnmaskedIPLoggingStatusDisabled + if d.Get("log_unmasked_ips_enabled").(bool) { + unmaskedIPLoggingStatus = security.UnmaskedIPLoggingStatusEnabled + } + solution := security.IoTSecuritySolutionModel{ + Location: utils.String(location), + IoTSecuritySolutionProperties: &security.IoTSecuritySolutionProperties{ + DisplayName: utils.String(d.Get("display_name").(string)), + Status: status, + Export: expandIotSecuritySolutionExport(d.Get("events_to_export").(*schema.Set).List()), + IotHubs: utils.ExpandStringSlice(d.Get("iothub_ids").(*schema.Set).List()), + RecommendationsConfiguration: expandIotSecuritySolutionRecommendation(d.Get("recommendations_enabled").([]interface{})), + UnmaskedIPLoggingStatus: unmaskedIPLoggingStatus, + }, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + } + + logAnalyticsWorkspaceId := d.Get("log_analytics_workspace_id").(string) + if logAnalyticsWorkspaceId != "" { + solution.IoTSecuritySolutionProperties.Workspace = utils.String(logAnalyticsWorkspaceId) + } + + query := d.Get("query_for_resources").(string) + querySubscriptions := d.Get("query_subscription_ids").(*schema.Set).List() + if query != "" || len(querySubscriptions) > 0 { + if query != "" && len(querySubscriptions) > 0 { + solution.UserDefinedResources = &security.UserDefinedResourcesProperties{ + Query: utils.String(query), + QuerySubscriptions: utils.ExpandStringSlice(querySubscriptions), + } + } else { + return fmt.Errorf("`query_for_resources` and `query_subscription_ids` must be set togetther") + } + } + + if _, err := client.CreateOrUpdate(ctx, resourceGroup, name, solution); err != nil { + return fmt.Errorf("creating/updating Security Center Iot Security Solution %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.SetId(resourceId) + return resourceIotSecuritySolutionRead(d, meta) +} + +func resourceIotSecuritySolutionRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).SecurityCenter.IotSecuritySolutionClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.IotSecuritySolutionID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Security Center Iot Security Solution %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("reading Security Center Iot Security Solution %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + + d.Set("name", id.Name) + d.Set("resource_group_name", id.ResourceGroup) + d.Set("location", location.NormalizeNilable(resp.Location)) + if prop := resp.IoTSecuritySolutionProperties; prop != nil { + d.Set("enabled", prop.Status == security.SolutionStatusEnabled) + d.Set("display_name", prop.DisplayName) + d.Set("iothub_ids", utils.FlattenStringSlice(prop.IotHubs)) + d.Set("log_analytics_workspace_id", prop.Workspace) + d.Set("log_unmasked_ips_enabled", prop.UnmaskedIPLoggingStatus == security.UnmaskedIPLoggingStatusEnabled) + if err := d.Set("events_to_export", flattenIotSecuritySolutionExport(prop.Export)); err != nil { + return fmt.Errorf("setting `events_to_export`: %s", err) + } + if err := d.Set("recommendations_enabled", flattenIotSecuritySolutionRecommendation(prop.RecommendationsConfiguration)); err != nil { + return fmt.Errorf("setting `recommendations_enabled`: %s", err) + } + if prop.UserDefinedResources != nil { + d.Set("query_for_resources", prop.UserDefinedResources.Query) + d.Set("query_subscription_ids", utils.FlattenStringSlice(prop.UserDefinedResources.QuerySubscriptions)) + } + } + + return tags.FlattenAndSet(d, resp.Tags) +} + +func resourceIotSecuritySolutionDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).SecurityCenter.IotSecuritySolutionClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.IotSecuritySolutionID(d.Id()) + if err != nil { + return err + } + + if _, err := client.Delete(ctx, id.ResourceGroup, id.Name); err != nil { + return fmt.Errorf("deleting Security Center Iot Security Solution %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + + return nil +} + +func expandIotSecuritySolutionExport(input []interface{}) *[]security.ExportData { + if len(input) == 0 || input[0] == nil { + return nil + } + result := make([]security.ExportData, 0) + for _, item := range input { + result = append(result, security.ExportData(item.(string))) + } + return &result +} + +func expandIotSecuritySolutionRecommendation(input []interface{}) *[]security.RecommendationConfigurationProperties { + if len(input) == 0 || input[0] == nil { + return nil + } + result := make([]security.RecommendationConfigurationProperties, 0) + v := input[0].(map[string]interface{}) + for k, item := range getRecommendationSchemaMap() { + status := security.Disabled + if v[item].(bool) { + status = security.Enabled + } + result = append(result, security.RecommendationConfigurationProperties{ + RecommendationType: k, + Status: status, + }) + } + return &result +} + +func flattenIotSecuritySolutionExport(input *[]security.ExportData) []interface{} { + result := make([]interface{}, 0) + if input != nil { + for _, item := range *input { + result = append(result, string(item)) + } + } + return result +} + +func flattenIotSecuritySolutionRecommendation(input *[]security.RecommendationConfigurationProperties) []interface{} { + if input == nil || len(*input) == 0 { + return []interface{}{} + } + result := make(map[string]interface{}) + schemaMap := getRecommendationSchemaMap() + for _, item := range *input { + if v, ok := schemaMap[item.RecommendationType]; ok { + result[v] = item.Status == security.Enabled + } + } + return []interface{}{result} +} + +func getRecommendationSchemaMap() map[security.RecommendationType]string { + return map[security.RecommendationType]string{ + security.IoTACRAuthentication: "acr_authentication", + security.IoTAgentSendsUnutilizedMessages: "agent_send_unutilized_msg", + security.IoTBaseline: "baseline", + security.IoTEdgeHubMemOptimize: "edge_hub_mem_optimize", + security.IoTEdgeLoggingOptions: "edge_logging_option", + security.IoTInconsistentModuleSettings: "inconsistent_module_settings", + security.IoTInstallAgent: "install_agent", + security.IoTIPFilterDenyAll: "ip_filter_deny_all", + security.IoTIPFilterPermissiveRule: "ip_filter_permissive_rule", + security.IoTOpenPorts: "open_ports", + security.IoTPermissiveFirewallPolicy: "permissive_firewall_policy", + security.IoTPermissiveInputFirewallRules: "permissive_input_firewall_rules", + security.IoTPermissiveOutputFirewallRules: "permissive_output_firewall_rules", + security.IoTPrivilegedDockerOptions: "privileged_docker_options", + security.IoTSharedCredentials: "shared_credentials", + security.IoTVulnerableTLSCipherSuite: "vulnerable_tls_cipher_suite", + } +} diff --git a/azurerm/internal/services/securitycenter/iot_security_solution_resource_test.go b/azurerm/internal/services/securitycenter/iot_security_solution_resource_test.go new file mode 100644 index 000000000000..8e7797366127 --- /dev/null +++ b/azurerm/internal/services/securitycenter/iot_security_solution_resource_test.go @@ -0,0 +1,212 @@ +package securitycenter_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/securitycenter/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +type IotSecuritySolutionResource struct { +} + +func TestAccIotSecuritySolution_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iot_security_solution", "test") + r := IotSecuritySolutionResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotSecuritySolution_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iot_security_solution", "test") + r := IotSecuritySolutionResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func TestAccIotSecuritySolution_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iot_security_solution", "test") + r := IotSecuritySolutionResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotSecuritySolution_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iot_security_solution", "test") + r := IotSecuritySolutionResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (IotSecuritySolutionResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + id, err := parse.IotSecuritySolutionID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.SecurityCenter.IotSecuritySolutionClient.Get(ctx, id.ResourceGroup, id.Name) + if err != nil { + return nil, fmt.Errorf("reading Security Center Iot Security Solution %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + + return utils.Bool(resp.ID != nil), nil +} + +func (r IotSecuritySolutionResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iot_security_solution" "test" { + name = "acctest-Iot-Security-Solution-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + display_name = "Iot Security Solution" + iothub_ids = [azurerm_iothub.test.id] +} +`, r.template(data), data.RandomInteger) +} + +func (r IotSecuritySolutionResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iot_security_solution" "import" { + name = azurerm_iot_security_solution.test.name + resource_group_name = azurerm_iot_security_solution.test.resource_group_name + location = azurerm_iot_security_solution.test.location + display_name = azurerm_iot_security_solution.test.display_name + iothub_ids = [azurerm_iothub.test.id] +} +`, r.basic(data)) +} + +func (r IotSecuritySolutionResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_client_config" "test" {} + +resource "azurerm_log_analytics_workspace" "test" { + name = "acctestLAW-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku = "PerGB2018" + retention_in_days = 30 +} + +resource "azurerm_iot_security_solution" "test" { + name = "acctest-Iot-Security-Solution-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + display_name = "Iot Security Solution" + iothub_ids = [azurerm_iothub.test.id] + enabled = true + log_unmasked_ips_enabled = true + log_analytics_workspace_id = azurerm_log_analytics_workspace.test.id + events_to_export = ["RawEvents"] + + recommendations_enabled { + acr_authentication = false + agent_send_unutilized_msg = false + baseline = false + edge_hub_mem_optimize = false + edge_logging_option = false + inconsistent_module_settings = false + install_agent = false + ip_filter_deny_all = false + ip_filter_permissive_rule = false + open_ports = false + permissive_firewall_policy = false + permissive_input_firewall_rules = false + permissive_output_firewall_rules = false + privileged_docker_options = false + shared_credentials = false + vulnerable_tls_cipher_suite = false + } + + query_for_resources = "where type != \"microsoft.devices/iothubs\" | where name contains \"iot\"" + query_subscription_ids = [data.azurerm_client_config.test.subscription_id] + + tags = { + "Env" : "Staging" + } +} +`, r.template(data), data.RandomInteger, data.RandomInteger) +} + +func (r IotSecuritySolutionResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-security-%d" + location = "%s" +} + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/azurerm/internal/services/securitycenter/parse/iot_security_solution.go b/azurerm/internal/services/securitycenter/parse/iot_security_solution.go new file mode 100644 index 000000000000..0441fae342a7 --- /dev/null +++ b/azurerm/internal/services/securitycenter/parse/iot_security_solution.go @@ -0,0 +1,69 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type IotSecuritySolutionId struct { + SubscriptionId string + ResourceGroup string + Name string +} + +func NewIotSecuritySolutionID(subscriptionId, resourceGroup, name string) IotSecuritySolutionId { + return IotSecuritySolutionId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + Name: name, + } +} + +func (id IotSecuritySolutionId) String() string { + segments := []string{ + fmt.Sprintf("Name %q", id.Name), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Iot Security Solution", segmentsStr) +} + +func (id IotSecuritySolutionId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Security/IoTSecuritySolutions/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.Name) +} + +// IotSecuritySolutionID parses a IotSecuritySolution ID into an IotSecuritySolutionId struct +func IotSecuritySolutionID(input string) (*IotSecuritySolutionId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := IotSecuritySolutionId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.Name, err = id.PopSegment("IoTSecuritySolutions"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/azurerm/internal/services/securitycenter/parse/iot_security_solution_test.go b/azurerm/internal/services/securitycenter/parse/iot_security_solution_test.go new file mode 100644 index 000000000000..2941231c581a --- /dev/null +++ b/azurerm/internal/services/securitycenter/parse/iot_security_solution_test.go @@ -0,0 +1,112 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/resourceid" +) + +var _ resourceid.Formatter = IotSecuritySolutionId{} + +func TestIotSecuritySolutionIDFormatter(t *testing.T) { + actual := NewIotSecuritySolutionID("12345678-1234-9876-4563-123456789012", "resGroup1", "solution1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestIotSecuritySolutionID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *IotSecuritySolutionId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1", + Expected: &IotSecuritySolutionId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + Name: "solution1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.SECURITY/IOTSECURITYSOLUTIONS/SOLUTION1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := IotSecuritySolutionID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/azurerm/internal/services/securitycenter/registration.go b/azurerm/internal/services/securitycenter/registration.go index f6d39819d59a..49452b5d6dd8 100644 --- a/azurerm/internal/services/securitycenter/registration.go +++ b/azurerm/internal/services/securitycenter/registration.go @@ -27,6 +27,7 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ "azurerm_advanced_threat_protection": resourceAdvancedThreatProtection(), + "azurerm_iot_security_solution": resourceIotSecuritySolution(), "azurerm_security_center_contact": resourceSecurityCenterContact(), "azurerm_security_center_setting": resourceSecurityCenterSetting(), "azurerm_security_center_subscription_pricing": resourceSecurityCenterSubscriptionPricing(), diff --git a/azurerm/internal/services/securitycenter/resourceids.go b/azurerm/internal/services/securitycenter/resourceids.go new file mode 100644 index 000000000000..20c97a1bd729 --- /dev/null +++ b/azurerm/internal/services/securitycenter/resourceids.go @@ -0,0 +1,3 @@ +package securitycenter + +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=IotSecuritySolution -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1 diff --git a/azurerm/internal/services/securitycenter/validate/iot_security_solution_id.go b/azurerm/internal/services/securitycenter/validate/iot_security_solution_id.go new file mode 100644 index 000000000000..abbd4e0e8a19 --- /dev/null +++ b/azurerm/internal/services/securitycenter/validate/iot_security_solution_id.go @@ -0,0 +1,23 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/securitycenter/parse" +) + +func IotSecuritySolutionID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.IotSecuritySolutionID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/azurerm/internal/services/securitycenter/validate/iot_security_solution_id_test.go b/azurerm/internal/services/securitycenter/validate/iot_security_solution_id_test.go new file mode 100644 index 000000000000..8bfcdb64f56d --- /dev/null +++ b/azurerm/internal/services/securitycenter/validate/iot_security_solution_id_test.go @@ -0,0 +1,76 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestIotSecuritySolutionID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/", + Valid: false, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Security/IoTSecuritySolutions/solution1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.SECURITY/IOTSECURITYSOLUTIONS/SOLUTION1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := IotSecuritySolutionID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/azurerm/internal/services/securitycenter/validate/iot_security_solution_name.go b/azurerm/internal/services/securitycenter/validate/iot_security_solution_name.go new file mode 100644 index 000000000000..ba6730d641fa --- /dev/null +++ b/azurerm/internal/services/securitycenter/validate/iot_security_solution_name.go @@ -0,0 +1,23 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func IotSecuritySolutionName(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + // The name attribute rules are : + // 1. can only contain letter, digit, '-', '.' or '_' + + if !regexp.MustCompile(`^([-a-zA-Z0-9_.])+$`).MatchString(v) { + errors = append(errors, fmt.Errorf("%s can only contain letter, digit, '-', '.' or '_'", v)) + } + + return +} diff --git a/azurerm/internal/services/securitycenter/validate/iot_security_solution_name_test.go b/azurerm/internal/services/securitycenter/validate/iot_security_solution_name_test.go new file mode 100644 index 000000000000..9df78d7462e9 --- /dev/null +++ b/azurerm/internal/services/securitycenter/validate/iot_security_solution_name_test.go @@ -0,0 +1,36 @@ +package validate + +import "testing" + +func TestIotSecuritySolutionName(t *testing.T) { + testData := []struct { + input string + expected bool + }{ + { + // empty + input: "", + expected: false, + }, + { + // basic example + input: "ab-c", + expected: true, + }, + { + // can't contain character other than letter, digit, '-', '.' and '_' + input: "ab*", + expected: false, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.input) + + _, errors := IotSecuritySolutionName(v.input, "name") + actual := len(errors) == 0 + if v.expected != actual { + t.Fatalf("Expected %t but got %t", v.expected, actual) + } + } +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 81731cae76ed..7d624f89732b 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -2776,6 +2776,10 @@ azurerm_advanced_threat_protection +