diff --git a/mmv1/products/securitycenter/FolderCustomModule.yaml b/mmv1/products/securitycenter/FolderCustomModule.yaml index aeb913646db1..8fa1ab78ce34 100644 --- a/mmv1/products/securitycenter/FolderCustomModule.yaml +++ b/mmv1/products/securitycenter/FolderCustomModule.yaml @@ -33,6 +33,7 @@ examples: name: "scc_folder_custom_module_basic" primary_resource_id: "example" pull_external: true + skip_test: true vars: folder_display_name: "folder-name" display_name: basic_custom_module @@ -45,6 +46,7 @@ examples: name: "scc_folder_custom_module_full" primary_resource_id: "example" pull_external: true + skip_test: true vars: folder_display_name: "folder-name" display_name: full_custom_module diff --git a/mmv1/products/securitycenter/OrganizationCustomModule.yaml b/mmv1/products/securitycenter/OrganizationCustomModule.yaml new file mode 100644 index 000000000000..34a02c80de9e --- /dev/null +++ b/mmv1/products/securitycenter/OrganizationCustomModule.yaml @@ -0,0 +1,220 @@ +# Copyright 2023 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- !ruby/object:Api::Resource +name: 'OrganizationCustomModule' +description: | + Represents an instance of a Security Health Analytics custom module, including + its full module name, display name, enablement state, and last updated time. + You can create a custom module at the organization, folder, or project level. + Custom modules that you create at the organization or folder level are inherited + by the child folders and projects. +references: !ruby/object:Api::Resource::ReferenceLinks + guides: + 'Overview of custom modules for Security Health Analytics': 'https://cloud.google.com/security-command-center/docs/custom-modules-sha-overview' + api: 'https://cloud.google.com/security-command-center/docs/reference/rest/v1/organizations.securityHealthAnalyticsSettings.customModules' +base_url: 'organizations/{{organization}}/securityHealthAnalyticsSettings/customModules' +self_link: 'organizations/{{organization}}/securityHealthAnalyticsSettings/customModules/{{name}}' +mutex: 'organizations/{{organization}}/securityHealthAnalyticsSettings/customModules' +update_verb: :PATCH +update_mask: true +examples: + - !ruby/object:Provider::Terraform::Examples + name: "scc_organization_custom_module_basic" + primary_resource_id: "example" + skip_test: true + vars: + display_name: basic_custom_module + test_env_vars: + org_id: :ORG_ID + test_vars_overrides: + sleep: "true" + - !ruby/object:Provider::Terraform::Examples + name: "scc_organization_custom_module_full" + primary_resource_id: "example" + skip_test: true + vars: + display_name: full_custom_module + test_env_vars: + org_id: :ORG_ID + test_vars_overrides: + sleep: "true" + +parameters: + - !ruby/object:Api::Type::String + name: 'organization' + immutable: true + required: true + url_param_only: true + description: | + Numerical ID of the parent organization. + +properties: + - !ruby/object:Api::Type::String + name: 'name' + output: true + custom_flatten: templates/terraform/custom_flatten/name_from_self_link.erb + description: | + The resource name of the custom module. Its format is "organizations/{org_id}/securityHealthAnalyticsSettings/customModules/{customModule}". + The id {customModule} is server-generated and is not user settable. It will be a numeric id containing 1-20 digits. + - !ruby/object:Api::Type::String + name: 'displayName' + immutable: true + required: true + # API error for invalid display names is just "INVALID_ARGUMENT" with no details + validation: !ruby/object:Provider::Terraform::Validation + function: 'verify.ValidateRegexp(`^[a-z][\w_]{0,127}$`)' + description: | + The display name of the Security Health Analytics custom module. This + display name becomes the finding category for all findings that are + returned by this custom module. The display name must be between 1 and + 128 characters, start with a lowercase letter, and contain alphanumeric + characters or underscores only. + - !ruby/object:Api::Type::Enum + name: 'enablementState' + required: true + description: | + The enablement state of the custom module. + values: + - :ENABLED + - :DISABLED + - !ruby/object:Api::Type::String + name: 'updateTime' + output: true + description: | + The time at which the custom module was last updated. + + A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and + up to nine fractional digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + - !ruby/object:Api::Type::String + name: 'lastEditor' + output: true + description: | + The editor that last updated the custom module. + - !ruby/object:Api::Type::String + name: 'ancestorModule' + output: true + description: | + If empty, indicates that the custom module was created in the organization, folder, + or project in which you are viewing the custom module. Otherwise, ancestor_module + specifies the organization or folder from which the custom module is inherited. + - !ruby/object:Api::Type::NestedObject + name: 'customConfig' + required: true + description: | + The user specified custom configuration for the module. + properties: + - !ruby/object:Api::Type::NestedObject + name: 'predicate' + required: true + description: | + The CEL expression to evaluate to produce findings. When the expression evaluates + to true against a resource, a finding is generated. + properties: + - !ruby/object:Api::Type::String + name: 'expression' + required: true + description: | + Textual representation of an expression in Common Expression Language syntax. + - !ruby/object:Api::Type::String + name: 'title' + description: | + Title for the expression, i.e. a short string describing its purpose. This can + be used e.g. in UIs which allow to enter the expression. + - !ruby/object:Api::Type::String + name: 'description' + description: | + Description of the expression. This is a longer text which describes the + expression, e.g. when hovered over it in a UI. + - !ruby/object:Api::Type::String + name: 'location' + description: | + String indicating the location of the expression for error reporting, e.g. a + file name and a position in the file. + - !ruby/object:Api::Type::NestedObject + name: 'customOutput' + description: | + Custom output properties. + properties: + - !ruby/object:Api::Type::Array + name: 'properties' + description: | + A list of custom output properties to add to the finding. + item_type: !ruby/object:Api::Type::NestedObject + properties: + - !ruby/object:Api::Type::String + name: 'name' + description: | + Name of the property for the custom output. + - !ruby/object:Api::Type::NestedObject + name: 'valueExpression' + description: | + The CEL expression for the custom output. A resource property can be specified + to return the value of the property or a text string enclosed in quotation marks. + properties: + - !ruby/object:Api::Type::String + name: 'expression' + required: true + description: | + Textual representation of an expression in Common Expression Language syntax. + - !ruby/object:Api::Type::String + name: 'title' + description: | + Title for the expression, i.e. a short string describing its purpose. This can + be used e.g. in UIs which allow to enter the expression. + - !ruby/object:Api::Type::String + name: 'description' + description: | + Description of the expression. This is a longer text which describes the + expression, e.g. when hovered over it in a UI. + - !ruby/object:Api::Type::String + name: 'location' + description: | + String indicating the location of the expression for error reporting, e.g. a + file name and a position in the file. + - !ruby/object:Api::Type::NestedObject + name: 'resourceSelector' + required: true + description: | + The resource types that the custom module operates on. Each custom module + can specify up to 5 resource types. + properties: + - !ruby/object:Api::Type::Array + name: 'resourceTypes' + required: true + description: | + The resource types to run the detector on. + item_type: Api::Type::String + - !ruby/object:Api::Type::Enum + name: 'severity' + required: true + description: | + The severity to assign to findings generated by the module. + values: + - :CRITICAL + - :HIGH + - :MEDIUM + - :LOW + - !ruby/object:Api::Type::String + name: 'description' + description: | + Text that describes the vulnerability or misconfiguration that the custom + module detects. This explanation is returned with each finding instance to + help investigators understand the detected issue. The text must be enclosed in quotation marks. + - !ruby/object:Api::Type::String + name: 'recommendation' + required: true + description: | + An explanation of the recommended steps that security teams can take to resolve + the detected issue. This explanation is returned with each finding generated by + this module in the nextSteps property of the finding JSON. diff --git a/mmv1/products/securitycenter/ProjectCustomModule.yaml b/mmv1/products/securitycenter/ProjectCustomModule.yaml index d5c8ede51f20..4eb5877ace61 100644 --- a/mmv1/products/securitycenter/ProjectCustomModule.yaml +++ b/mmv1/products/securitycenter/ProjectCustomModule.yaml @@ -32,11 +32,13 @@ examples: - !ruby/object:Provider::Terraform::Examples name: "scc_project_custom_module_basic" primary_resource_id: "example" + skip_test: true vars: display_name: basic_custom_module - !ruby/object:Provider::Terraform::Examples name: "scc_project_custom_module_full" primary_resource_id: "example" + skip_test: true vars: display_name: full_custom_module diff --git a/mmv1/templates/terraform/examples/scc_folder_custom_module_basic.tf.erb b/mmv1/templates/terraform/examples/scc_folder_custom_module_basic.tf.erb index d40e0c6616b7..5a56b75249da 100644 --- a/mmv1/templates/terraform/examples/scc_folder_custom_module_basic.tf.erb +++ b/mmv1/templates/terraform/examples/scc_folder_custom_module_basic.tf.erb @@ -2,33 +2,22 @@ resource "google_folder" "folder" { parent = "organizations/<%= ctx[:test_env_vars]['org_id'] %>" display_name = "<%= ctx[:vars]['folder_display_name'] %>" } -<%- unless ctx[:vars]['sleep'].empty? %> -resource "time_sleep" "wait_1_minute" { - depends_on = [google_folder.folder] - - create_duration = "1m" -} -<% end -%> resource "google_scc_folder_custom_module" "<%= ctx[:primary_resource_id] %>" { - folder = google_folder.folder.folder_id - display_name = "<%= ctx[:vars]['display_name'] %>" - enablement_state = "ENABLED" - custom_config { - predicate { - expression = "resource.rotationPeriod > duration(\"2592000s\")" - } - resource_selector { - resource_types = [ - "cloudkms.googleapis.com/CryptoKey", - ] - } - description = "The rotation period of the identified cryptokey resource exceeds 30 days." - recommendation = "Set the rotation period to at most 30 days." - severity = "MEDIUM" - } - -<%- unless ctx[:vars]['sleep'].empty? %> - depends_on = [time_sleep.wait_1_minute] -<% end -%> + folder = google_folder.folder.folder_id + display_name = "<%= ctx[:vars]['display_name'] %>" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + } } \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/scc_folder_custom_module_full.tf.erb b/mmv1/templates/terraform/examples/scc_folder_custom_module_full.tf.erb index dafbe422c55e..b54bcd04c701 100644 --- a/mmv1/templates/terraform/examples/scc_folder_custom_module_full.tf.erb +++ b/mmv1/templates/terraform/examples/scc_folder_custom_module_full.tf.erb @@ -2,47 +2,36 @@ resource "google_folder" "folder" { parent = "organizations/<%= ctx[:test_env_vars]['org_id'] %>" display_name = "<%= ctx[:vars]['folder_display_name'] %>" } -<%- unless ctx[:vars]['sleep'].empty? %> -resource "time_sleep" "wait_1_minute" { - depends_on = [google_folder.folder] - - create_duration = "1m" -} -<% end -%> resource "google_scc_folder_custom_module" "<%= ctx[:primary_resource_id] %>" { - folder = google_folder.folder.folder_id - display_name = "<%= ctx[:vars]['display_name'] %>" - enablement_state = "ENABLED" - custom_config { - predicate { - expression = "resource.rotationPeriod > duration(\"2592000s\")" - title = "Purpose of the expression" - description = "description of the expression" - location = "location of the expression" - } - custom_output { - properties { - name = "duration" - value_expression { - expression = "resource.rotationPeriod" - title = "Purpose of the expression" - description = "description of the expression" - location = "location of the expression" - } - } - } - resource_selector { - resource_types = [ - "cloudkms.googleapis.com/CryptoKey", - ] - } - severity = "LOW" - description = "Description of the custom module" - recommendation = "Steps to resolve violation" - } - -<%- unless ctx[:vars]['sleep'].empty? %> - depends_on = [time_sleep.wait_1_minute] -<% end -%> + folder = google_folder.folder.folder_id + display_name = "<%= ctx[:vars]['display_name'] %>" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + custom_output { + properties { + name = "duration" + value_expression { + expression = "resource.rotationPeriod" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + } + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + severity = "LOW" + description = "Description of the custom module" + recommendation = "Steps to resolve violation" + } } \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/scc_organization_custom_module_basic.tf.erb b/mmv1/templates/terraform/examples/scc_organization_custom_module_basic.tf.erb new file mode 100644 index 000000000000..841f40f5be13 --- /dev/null +++ b/mmv1/templates/terraform/examples/scc_organization_custom_module_basic.tf.erb @@ -0,0 +1,18 @@ +resource "google_scc_organization_custom_module" "<%= ctx[:primary_resource_id] %>" { + organization = "<%= ctx[:test_env_vars]['org_id'] %>" + display_name = "<%= ctx[:vars]['display_name'] %>" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + } +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/scc_organization_custom_module_full.tf.erb b/mmv1/templates/terraform/examples/scc_organization_custom_module_full.tf.erb new file mode 100644 index 000000000000..2737451ae4c3 --- /dev/null +++ b/mmv1/templates/terraform/examples/scc_organization_custom_module_full.tf.erb @@ -0,0 +1,32 @@ +resource "google_scc_organization_custom_module" "<%= ctx[:primary_resource_id] %>" { + organization = "<%= ctx[:test_env_vars]['org_id'] %>" + display_name = "<%= ctx[:vars]['display_name'] %>" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + custom_output { + properties { + name = "duration" + value_expression { + expression = "resource.rotationPeriod" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + } + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + severity = "LOW" + description = "Description of the custom module" + recommendation = "Steps to resolve violation" + } +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/scc_project_custom_module_basic.tf.erb b/mmv1/templates/terraform/examples/scc_project_custom_module_basic.tf.erb index 0494a48df1f1..8ec2a4bb9785 100644 --- a/mmv1/templates/terraform/examples/scc_project_custom_module_basic.tf.erb +++ b/mmv1/templates/terraform/examples/scc_project_custom_module_basic.tf.erb @@ -1,17 +1,17 @@ resource "google_scc_project_custom_module" "<%= ctx[:primary_resource_id] %>" { - display_name = "<%= ctx[:vars]['display_name'] %>" - enablement_state = "ENABLED" - custom_config { - predicate { - expression = "resource.rotationPeriod > duration(\"2592000s\")" - } - resource_selector { - resource_types = [ - "cloudkms.googleapis.com/CryptoKey", - ] - } - description = "The rotation period of the identified cryptokey resource exceeds 30 days." - recommendation = "Set the rotation period to at most 30 days." - severity = "MEDIUM" - } + display_name = "<%= ctx[:vars]['display_name'] %>" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + } } \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/scc_project_custom_module_full.tf.erb b/mmv1/templates/terraform/examples/scc_project_custom_module_full.tf.erb index cb3c1473445f..4b8ed23aa7ad 100644 --- a/mmv1/templates/terraform/examples/scc_project_custom_module_full.tf.erb +++ b/mmv1/templates/terraform/examples/scc_project_custom_module_full.tf.erb @@ -1,31 +1,31 @@ resource "google_scc_project_custom_module" "<%= ctx[:primary_resource_id] %>" { - display_name = "<%= ctx[:vars]['display_name'] %>" - enablement_state = "ENABLED" - custom_config { - predicate { - expression = "resource.rotationPeriod > duration(\"2592000s\")" - title = "Purpose of the expression" - description = "description of the expression" - location = "location of the expression" - } - custom_output { - properties { - name = "duration" - value_expression { - expression = "resource.rotationPeriod" - title = "Purpose of the expression" - description = "description of the expression" - location = "location of the expression" - } - } - } - resource_selector { - resource_types = [ - "cloudkms.googleapis.com/CryptoKey", - ] - } - severity = "LOW" - description = "Description of the custom module" - recommendation = "Steps to resolve violation" - } + display_name = "<%= ctx[:vars]['display_name'] %>" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + custom_output { + properties { + name = "duration" + value_expression { + expression = "resource.rotationPeriod" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + } + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + severity = "LOW" + description = "Description of the custom module" + recommendation = "Steps to resolve violation" + } } \ No newline at end of file diff --git a/mmv1/third_party/terraform/services/securitycenter/resource_scc_folder_custom_module_test.go b/mmv1/third_party/terraform/services/securitycenter/resource_scc_folder_custom_module_test.go index 2460d94c6b1b..40db5dfe0b5e 100644 --- a/mmv1/third_party/terraform/services/securitycenter/resource_scc_folder_custom_module_test.go +++ b/mmv1/third_party/terraform/services/securitycenter/resource_scc_folder_custom_module_test.go @@ -1,19 +1,27 @@ package securitycenter_test import ( + "fmt" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-google/google/acctest" "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" ) -func TestAccSecurityCenterFolderCustomModule_sccFolderCustomModuleUpdate(t *testing.T) { +// Custom Module tests cannot be run in parallel without running into 409 Conflict reponses. +// Run them as individual steps of an update test instead. +func TestAccSecurityCenterFolderCustomModule(t *testing.T) { t.Parallel() context := map[string]interface{}{ "org_id": envvar.GetTestOrgFromEnv(t), + "sleep": true, "random_suffix": acctest.RandString(t, 10), } @@ -26,26 +34,116 @@ func TestAccSecurityCenterFolderCustomModule_sccFolderCustomModuleUpdate(t *test }, CheckDestroy: testAccCheckSecurityCenterFolderCustomModuleDestroyProducer(t), Steps: []resource.TestStep{ + { + Config: testAccSecurityCenterFolderCustomModule_sccFolderCustomModuleBasicExample(context), + }, + { + ResourceName: "google_scc_folder_custom_module.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"folder"}, + }, { Config: testAccSecurityCenterFolderCustomModule_sccFolderCustomModuleFullExample(context), }, { - ResourceName: "google_scc_folder_custom_module.example", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_scc_folder_custom_module.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"folder"}, }, { Config: testAccSecurityCenterFolderCustomModule_sccFolderCustomModuleUpdate(context), }, { - ResourceName: "google_scc_folder_custom_module.example", - ImportState: true, - ImportStateVerify: true, + ResourceName: "google_scc_folder_custom_module.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"folder"}, }, }, }) } +func testAccSecurityCenterFolderCustomModule_sccFolderCustomModuleBasicExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_folder" "folder" { + parent = "organizations/%{org_id}" + display_name = "tf-test-folder-name%{random_suffix}" +} + +resource "time_sleep" "wait_1_minute" { + depends_on = [google_folder.folder] + + create_duration = "1m" +} + +resource "google_scc_folder_custom_module" "example" { + folder = google_folder.folder.folder_id + display_name = "tf_test_basic_custom_module%{random_suffix}" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + } + + + depends_on = [time_sleep.wait_1_minute] +} +`, context) +} + +func testAccSecurityCenterFolderCustomModule_sccFolderCustomModuleFullExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_folder" "folder" { + parent = "organizations/%{org_id}" + display_name = "tf-test-folder-name%{random_suffix}" +} + +resource "google_scc_folder_custom_module" "example" { + folder = google_folder.folder.folder_id + display_name = "tf_test_full_custom_module%{random_suffix}" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + custom_output { + properties { + name = "duration" + value_expression { + expression = "resource.rotationPeriod" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + } + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + severity = "LOW" + description = "Description of the custom module" + recommendation = "Steps to resolve violation" + } +} +`, context) +} + func testAccSecurityCenterFolderCustomModule_sccFolderCustomModuleUpdate(context map[string]interface{}) string { return acctest.Nprintf(` resource "google_folder" "folder" { @@ -87,3 +185,42 @@ resource "google_scc_folder_custom_module" "example" { } `, context) } + +func testAccCheckSecurityCenterFolderCustomModuleDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_scc_folder_custom_module" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := acctest.GoogleProviderConfig(t) + + url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{SecurityCenterBasePath}}folders/{{folder}}/securityHealthAnalyticsSettings/customModules/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err == nil { + return fmt.Errorf("SecurityCenterFolderCustomModule still exists at %s", url) + } + } + + return nil + } +} diff --git a/mmv1/third_party/terraform/services/securitycenter/resource_scc_organization_custom_module_test.go b/mmv1/third_party/terraform/services/securitycenter/resource_scc_organization_custom_module_test.go new file mode 100644 index 000000000000..8869eb7a5b96 --- /dev/null +++ b/mmv1/third_party/terraform/services/securitycenter/resource_scc_organization_custom_module_test.go @@ -0,0 +1,197 @@ +package securitycenter_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +// Custom Module tests cannot be run in parallel without running into 409 Conflict reponses. +// Run them as individual steps of an update test instead. +func TestAccSecurityCenterOrganizationCustomModule(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "org_id": envvar.GetTestOrgFromEnv(t), + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckSecurityCenterOrganizationCustomModuleDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSecurityCenterOrganizationCustomModule_sccOrganizationCustomModuleBasicExample(context), + }, + { + ResourceName: "google_scc_organization_custom_module.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"organization"}, + }, + { + Config: testAccSecurityCenterOrganizationCustomModule_sccOrganizationCustomModuleFullExample(context), + }, + { + ResourceName: "google_scc_organization_custom_module.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"organization"}, + }, + { + Config: testAccSecurityCenterOrganizationCustomModule_sccOrganizationCustomModuleUpdate(context), + }, + { + ResourceName: "google_scc_organization_custom_module.example", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"organization"}, + }, + }, + }) +} + +func testAccSecurityCenterOrganizationCustomModule_sccOrganizationCustomModuleBasicExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_scc_organization_custom_module" "example" { + organization = "%{org_id}" + display_name = "tf_test_basic_custom_module%{random_suffix}" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + } +} +`, context) +} + +func testAccSecurityCenterOrganizationCustomModule_sccOrganizationCustomModuleFullExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_scc_organization_custom_module" "example" { + organization = "%{org_id}" + display_name = "tf_test_full_custom_module%{random_suffix}" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + custom_output { + properties { + name = "duration" + value_expression { + expression = "resource.rotationPeriod" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + } + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + severity = "LOW" + description = "Description of the custom module" + recommendation = "Steps to resolve violation" + } +} +`, context) +} + +func testAccSecurityCenterOrganizationCustomModule_sccOrganizationCustomModuleUpdate(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_scc_organization_custom_module" "example" { + organization = "%{org_id}" + display_name = "tf_test_full_custom_module%{random_suffix}" + enablement_state = "DISABLED" + custom_config { + predicate { + expression = "resource.name == \"updated-name\"" + title = "Updated expression title" + description = "Updated description of the expression" + location = "Updated location of the expression" + } + custom_output { + properties { + name = "violation" + value_expression { + expression = "resource.name" + title = "Updated expression title" + description = "Updated description of the expression" + location = "Updated location of the expression" + } + } + } + resource_selector { + resource_types = [ + "compute.googleapis.com/Instance", + ] + } + severity = "CRITICAL" + description = "Updated description of the custom module" + recommendation = "Updated steps to resolve violation" + } +} +`, context) +} + +func testAccCheckSecurityCenterOrganizationCustomModuleDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_scc_organization_custom_module" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := acctest.GoogleProviderConfig(t) + + url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{SecurityCenterBasePath}}organizations/{{organization}}/securityHealthAnalyticsSettings/customModules/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err == nil { + return fmt.Errorf("SecurityCenterOrganizationCustomModule still exists at %s", url) + } + } + + return nil + } +} diff --git a/mmv1/third_party/terraform/services/securitycenter/resource_scc_project_custom_module_test.go b/mmv1/third_party/terraform/services/securitycenter/resource_scc_project_custom_module_test.go index 37e73a4e6cce..84df80c79b06 100644 --- a/mmv1/third_party/terraform/services/securitycenter/resource_scc_project_custom_module_test.go +++ b/mmv1/third_party/terraform/services/securitycenter/resource_scc_project_custom_module_test.go @@ -1,14 +1,21 @@ package securitycenter_test import ( + "fmt" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" ) -func TestAccSecurityCenterProjectCustomModule_sccProjectCustomModuleUpdate(t *testing.T) { +// Custom Module tests cannot be run in parallel without running into 409 Conflict reponses. +// Run them as individual steps of an update test instead. +func TestAccSecurityCenterProjectCustomModule(t *testing.T) { t.Parallel() context := map[string]interface{}{ @@ -20,6 +27,14 @@ func TestAccSecurityCenterProjectCustomModule_sccProjectCustomModuleUpdate(t *te ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), CheckDestroy: testAccCheckSecurityCenterProjectCustomModuleDestroyProducer(t), Steps: []resource.TestStep{ + { + Config: testAccSecurityCenterProjectCustomModule_sccProjectCustomModuleBasicExample(context), + }, + { + ResourceName: "google_scc_project_custom_module.example", + ImportState: true, + ImportStateVerify: true, + }, { Config: testAccSecurityCenterProjectCustomModule_sccProjectCustomModuleFullExample(context), }, @@ -40,6 +55,64 @@ func TestAccSecurityCenterProjectCustomModule_sccProjectCustomModuleUpdate(t *te }) } +func testAccSecurityCenterProjectCustomModule_sccProjectCustomModuleBasicExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_scc_project_custom_module" "example" { + display_name = "tf_test_basic_custom_module%{random_suffix}" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + } +} +`, context) +} + +func testAccSecurityCenterProjectCustomModule_sccProjectCustomModuleFullExample(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_scc_project_custom_module" "example" { + display_name = "tf_test_full_custom_module%{random_suffix}" + enablement_state = "ENABLED" + custom_config { + predicate { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + custom_output { + properties { + name = "duration" + value_expression { + expression = "resource.rotationPeriod" + title = "Purpose of the expression" + description = "description of the expression" + location = "location of the expression" + } + } + } + resource_selector { + resource_types = [ + "cloudkms.googleapis.com/CryptoKey", + ] + } + severity = "LOW" + description = "Description of the custom module" + recommendation = "Steps to resolve violation" + } +} +`, context) +} + func testAccSecurityCenterProjectCustomModule_sccProjectCustomModuleUpdate(context map[string]interface{}) string { return acctest.Nprintf(` resource "google_scc_project_custom_module" "example" { @@ -75,3 +148,42 @@ resource "google_scc_project_custom_module" "example" { } `, context) } + +func testAccCheckSecurityCenterProjectCustomModuleDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_scc_project_custom_module" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := acctest.GoogleProviderConfig(t) + + url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{SecurityCenterBasePath}}projects/{{project}}/securityHealthAnalyticsSettings/customModules/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err == nil { + return fmt.Errorf("SecurityCenterProjectCustomModule still exists at %s", url) + } + } + + return nil + } +}