diff --git a/azurerm/config.go b/azurerm/config.go index f4414a901a14..e53d6ea028c3 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -258,8 +258,9 @@ type ArmClient struct { searchServicesClient search.ServicesClient // Security Centre - securityCenterPricingClient security.PricingsClient - securityCenterContactsClient security.ContactsClient + securityCenterPricingClient security.PricingsClient + securityCenterContactsClient security.ContactsClient + securityCenterWorkspaceClient security.WorkspaceSettingsClient // ServiceBus serviceBusQueuesClient servicebus.QueuesClient @@ -1040,6 +1041,10 @@ func (c *ArmClient) registerSecurityCenterClients(endpoint, subscriptionId, ascL securityCenterContactsClient := security.NewContactsClientWithBaseURI(endpoint, subscriptionId, ascLocation) c.configureClient(&securityCenterContactsClient.Client, auth) c.securityCenterContactsClient = securityCenterContactsClient + + securityCenterWorkspaceClient := security.NewWorkspaceSettingsClientWithBaseURI(endpoint, subscriptionId, ascLocation) + c.configureClient(&securityCenterWorkspaceClient.Client, auth) + c.securityCenterWorkspaceClient = securityCenterWorkspaceClient } func (c *ArmClient) registerServiceBusClients(endpoint, subscriptionId string, auth autorest.Authorizer) { diff --git a/azurerm/provider.go b/azurerm/provider.go index 40c1dea5a6f8..08cf4b539ec0 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -248,6 +248,7 @@ func Provider() terraform.ResourceProvider { "azurerm_search_service": resourceArmSearchService(), "azurerm_security_center_subscription_pricing": resourceArmSecurityCenterSubscriptionPricing(), "azurerm_security_center_contact": resourceArmSecurityCenterContact(), + "azurerm_security_center_workspace": resourceArmSecurityCenterWorkspace(), "azurerm_servicebus_namespace": resourceArmServiceBusNamespace(), "azurerm_servicebus_namespace_authorization_rule": resourceArmServiceBusNamespaceAuthorizationRule(), "azurerm_servicebus_queue": resourceArmServiceBusQueue(), diff --git a/azurerm/resource_arm_security_center_contact.go b/azurerm/resource_arm_security_center_contact.go index 6d1d24529d01..027b9f37d691 100644 --- a/azurerm/resource_arm_security_center_contact.go +++ b/azurerm/resource_arm_security_center_contact.go @@ -2,11 +2,12 @@ package azurerm import ( "fmt" + "log" + "github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/2017-08-01-preview/security" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" - "log" ) //seems you can only set one contact: diff --git a/azurerm/resource_arm_security_center_subscription_pricing_test.go b/azurerm/resource_arm_security_center_subscription_pricing_test.go index cc3504667fd3..6629cdcafa44 100644 --- a/azurerm/resource_arm_security_center_subscription_pricing_test.go +++ b/azurerm/resource_arm_security_center_subscription_pricing_test.go @@ -9,7 +9,7 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) -func TestAccAzureRMSecurityCenterSubscriptionPricing_update(t *testing.T) { +func testAccAzureRMSecurityCenterSubscriptionPricing_update(t *testing.T) { resourceName := "azurerm_security_center_subscription_pricing.test" resource.Test(t, resource.TestCase{ diff --git a/azurerm/resource_arm_security_center_test.go b/azurerm/resource_arm_security_center_test.go new file mode 100644 index 000000000000..4a3ed04a4b07 --- /dev/null +++ b/azurerm/resource_arm_security_center_test.go @@ -0,0 +1,31 @@ +package azurerm + +import ( + "testing" +) + +func TestAccAzureRMSecurityCenter_pricingAndWorkspace(t *testing.T) { + // NOTE: this is a combined test rather than separate split out tests + // due to the workspace tests depending on the current pricing tier + testCases := map[string]map[string]func(t *testing.T){ + "pricing": { + "update": testAccAzureRMSecurityCenterSubscriptionPricing_update, + }, + "workspace": { + "basic": testAccAzureRMSecurityCenterWorkspace_basic, + "update": testAccAzureRMSecurityCenterWorkspace_update, + }, + } + + for group, m := range testCases { + m := m + t.Run(group, func(t *testing.T) { + for name, tc := range m { + tc := tc + t.Run(name, func(t *testing.T) { + tc(t) + }) + } + }) + } +} diff --git a/azurerm/resource_arm_security_center_workspace.go b/azurerm/resource_arm_security_center_workspace.go new file mode 100644 index 000000000000..670deda4657d --- /dev/null +++ b/azurerm/resource_arm_security_center_workspace.go @@ -0,0 +1,161 @@ +package azurerm + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/2017-08-01-preview/security" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +//only valid name is default +// Message="Invalid workspace settings name 'kttest' , only default is allowed " +const securityCenterWorkspaceName = "default" + +func resourceArmSecurityCenterWorkspace() *schema.Resource { + return &schema.Resource{ + Create: resourceArmSecurityCenterWorkspaceCreateUpdate, + Read: resourceArmSecurityCenterWorkspaceRead, + Update: resourceArmSecurityCenterWorkspaceCreateUpdate, + Delete: resourceArmSecurityCenterWorkspaceDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "scope": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + + "workspace_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: azure.ValidateResourceID, + }, + }, + } +} + +func resourceArmSecurityCenterWorkspaceCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).securityCenterWorkspaceClient + ctx := meta.(*ArmClient).StopContext + + priceClient := meta.(*ArmClient).securityCenterPricingClient + + //get pricing tier, workspace can only be configured when tier is not Free. + //API does not error, it just doesn't set the workspace scope + price, err := priceClient.GetSubscriptionPricing(ctx, securityCenterSubscriptionPricingName) + if err != nil { + return fmt.Errorf("Error reading Security Center Subscription pricing: %+v", err) + } + + if price.PricingProperties == nil { + return fmt.Errorf("Security Center Subscription pricing propertier is nil") + } + if price.PricingProperties.PricingTier == security.Free { + return fmt.Errorf("Security Center Subscription workspace cannot be set when pricing tier is `Free`") + } + + name := securityCenterWorkspaceName + + contact := security.WorkspaceSetting{ + WorkspaceSettingProperties: &security.WorkspaceSettingProperties{ + Scope: utils.String(d.Get("scope").(string)), + WorkspaceID: utils.String(d.Get("workspace_id").(string)), + }, + } + + if d.IsNewResource() { + _, err := client.Create(ctx, name, contact) + if err != nil { + return fmt.Errorf("Error creating Security Center Workspace: %+v", err) + } + } else { + _, err := client.Update(ctx, name, contact) + if err != nil { + return fmt.Errorf("Error updating Security Center Workspace: %+v", err) + } + } + + //api returns "" for workspace id after an create/update and eventually the new value + stateConf := &resource.StateChangeConf{ + Pending: []string{"Waiting"}, + Target: []string{"Populated"}, + Timeout: 15 * time.Minute, + MinTimeout: 30 * time.Second, + Refresh: func() (interface{}, string, error) { + + resp, err := client.Get(ctx, name) + if err != nil { + return resp, "Error", fmt.Errorf("Error reading Security Center Workspace: %+v", err) + } + + if properties := resp.WorkspaceSettingProperties; properties != nil { + if properties.WorkspaceID != nil && *properties.WorkspaceID != "" { + return resp, "Populated", nil + } + } + + return resp, "Waiting", nil + }, + } + + resp, err := stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting: %+v", err) + } + + if d.IsNewResource() { + d.SetId(*resp.(security.WorkspaceSetting).ID) + } + + return resourceArmSecurityCenterWorkspaceRead(d, meta) +} + +func resourceArmSecurityCenterWorkspaceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).securityCenterWorkspaceClient + ctx := meta.(*ArmClient).StopContext + + resp, err := client.Get(ctx, securityCenterWorkspaceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Security Center Subscription Workspace was not found: %v", err) + d.SetId("") + return nil + } + + return fmt.Errorf("Error reading Security Center Workspace: %+v", err) + } + + if properties := resp.WorkspaceSettingProperties; properties != nil { + d.Set("scope", properties.Scope) + d.Set("workspace_id", properties.WorkspaceID) + } + + return nil +} + +func resourceArmSecurityCenterWorkspaceDelete(_ *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).securityCenterWorkspaceClient + ctx := meta.(*ArmClient).StopContext + + resp, err := client.Delete(ctx, securityCenterWorkspaceName) + if err != nil { + if utils.ResponseWasNotFound(resp) { + log.Printf("[DEBUG] Security Center Subscription Workspace was not found: %v", err) + return nil + } + + return fmt.Errorf("Error deleting Security Center Workspace: %+v", err) + } + + return nil +} diff --git a/azurerm/resource_arm_security_center_workspace_test.go b/azurerm/resource_arm_security_center_workspace_test.go new file mode 100644 index 000000000000..01497ab5f9d3 --- /dev/null +++ b/azurerm/resource_arm_security_center_workspace_test.go @@ -0,0 +1,180 @@ +package azurerm + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func testAccAzureRMSecurityCenterWorkspace_basic(t *testing.T) { + resourceName := "azurerm_security_center_workspace.test" + ri := acctest.RandInt() + + scope := fmt.Sprintf("/subscriptions/%s", os.Getenv("ARM_SUBSCRIPTION_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMSecurityCenterWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMSecurityCenterWorkspace_basicCfg(ri, testLocation(), scope), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSecurityCenterWorkspaceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "scope", scope), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + //reset pricing to free + Config: testAccAzureRMSecurityCenterSubscriptionPricing_tier("Free"), + }, + }, + }) +} + +func testAccAzureRMSecurityCenterWorkspace_update(t *testing.T) { + resourceName := "azurerm_security_center_workspace.test" + ri := acctest.RandInt() + + scope := fmt.Sprintf("/subscriptions/%s", os.Getenv("ARM_SUBSCRIPTION_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMSecurityCenterWorkspace_basicCfg(ri, testLocation(), scope), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSecurityCenterWorkspaceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "scope", scope), + ), + }, + { + Config: testAccAzureRMSecurityCenterWorkspace_differentWorkspaceCfg(ri, testLocation(), scope), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMSecurityCenterWorkspaceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "scope", scope), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + //reset pricing to free + Config: testAccAzureRMSecurityCenterSubscriptionPricing_tier("Free"), + }, + }, + }) +} + +func testCheckAzureRMSecurityCenterWorkspaceExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).securityCenterWorkspaceClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + contactName := rs.Primary.Attributes["workspaceSettings"] + + resp, err := client.Get(ctx, contactName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Security Center Subscription Workspace %q was not found: %+v", contactName, err) + } + + return fmt.Errorf("Bad: GetWorkspace: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMSecurityCenterWorkspaceDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).securityCenterWorkspaceClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, res := range s.RootModule().Resources { + if res.Type != "azurerm_security_center_workspace" { + continue + } + + resp, err := client.Get(ctx, securityCenterWorkspaceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + + return err + } + + return fmt.Errorf("security center worspace settings still exists") + } + + return nil +} + +func testAccAzureRMSecurityCenterWorkspace_basicCfg(rInt int, location, scope string) string { + return fmt.Sprintf(` + +resource "azurerm_security_center_subscription_pricing" "test" { + tier = "Standard" +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_log_analytics_workspace" "test1" { + name = "acctest-%[1]d-1" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku = "PerGB2018" +} + +resource "azurerm_security_center_workspace" "test" { + scope = "%[3]s" + workspace_id = "${azurerm_log_analytics_workspace.test1.id}" +} +`, rInt, location, scope) +} + +func testAccAzureRMSecurityCenterWorkspace_differentWorkspaceCfg(rInt int, location, scope string) string { + return fmt.Sprintf(` +resource "azurerm_security_center_subscription_pricing" "test" { + tier = "Standard" +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_log_analytics_workspace" "test2" { + name = "acctest-%[1]d-2" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku = "PerGB2018" +} + +resource "azurerm_security_center_workspace" "test" { + scope = "%[3]s" + workspace_id = "${azurerm_log_analytics_workspace.test2.id}" +} +`, rInt, location, scope) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 0831edc7afc7..558488f49363 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1005,6 +1005,11 @@ azurerm_security_center_subscription_pricing + > diff --git a/website/docs/r/security_center_contact.markdown b/website/docs/r/security_center_contact.markdown index b66bd61d7678..52a5fdcc0d99 100644 --- a/website/docs/r/security_center_contact.markdown +++ b/website/docs/r/security_center_contact.markdown @@ -10,6 +10,8 @@ description: |- Manages the subscription's Security Center Contact. +~> **NOTE:** Owner access permission is required. + ## Example Usage ```hcl diff --git a/website/docs/r/security_center_workspace.markdown b/website/docs/r/security_center_workspace.markdown new file mode 100644 index 000000000000..6ed8aaaea36e --- /dev/null +++ b/website/docs/r/security_center_workspace.markdown @@ -0,0 +1,57 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_security_center_workspace" +sidebar_current: "docs-azurerm-security-center-workspace" +description: |- + Manages the subscription's Security Center Workspace. +--- + +# azurerm_security_center_workspace + +Manages the subscription's Security Center Workspace. + +~> **NOTE:** Owner access permission is required. + +~> **NOTE:** The subscription's pricing model can not be `Free` for this to have any affect. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "tfex-security-workspace" + location = "westus" +} + +resource "azurerm_log_analytics_workspace" "example" { + name = "tfex-security-workspace" + location = "${azurerm_resource_group.example.location}" + resource_group_name = "${azurerm_resource_group.example.name}" + sku = "PerGB2018" +} + +resource "azurerm_security_center_contact" "example" { + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + workspace_id = "${azurerm_log_analytics_workspace.example.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `scope` - (Required) The scope of VMs to send their security data to the desired workspace, unless overridden by a setting with more specific scope. +* `workspace_id` - (Required) The resource ID of the log analytics workspace to save the data in. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Security Center Contact ID. + +## Import + +The contact can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_security_center_workspace.example /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Security/workspaceSettings/default +```