diff --git a/modules/folder/README.md b/modules/folder/README.md index 05dfdbc9a..8798cb33d 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -11,6 +11,8 @@ This module allows the creation and management of folders, including support for - [Hierarchical Firewall Policy Attachments](#hierarchical-firewall-policy-attachments) - [Log Sinks](#log-sinks) - [Data Access Logs](#data-access-logs) +- [Custom Security Health Analytics Modules](#custom-security-health-analytics-modules) + - [Custom Security Health Analytics Modules Factory](#custom-security-health-analytics-modules-factory) - [Tags](#tags) - [Files](#files) - [Variables](#variables) @@ -372,8 +374,65 @@ module "folder" { # tftest modules=1 resources=3 inventory=logging-data-access.yaml e2e ``` +## Custom Security Health Analytics Modules + +[Security Health Analytics custom modules](https://cloud.google.com/security-command-center/docs/custom-modules-sha-create) can be defined via the `scc_sha_custom_modules` variable: + +```hcl +module "folder" { + source = "./fabric/modules/folder" + parent = var.folder_id + name = "Folder name" + scc_sha_custom_modules = { + cloudkmKeyRotationPeriod = { + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + predicate = { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector = { + resource_types = ["cloudkms.googleapis.com/CryptoKey"] + } + } + } +} +# tftest modules=1 resources=2 inventory=custom-modules-sha.yaml +``` +### Custom Security Health Analytics Modules Factory + +Custom modules can also be specified via a factory. Each file is mapped to a custom module, where the module name defaults to the file name. + +Custom modules defined via the variable are merged with those coming from the factory, and override them in case of duplicate names. + +```hcl +module "folder" { + source = "./fabric/modules/folder" + parent = var.folder_id + name = "Folder name" + factories_config = { + scc_sha_custom_modules = "data/scc_sha_custom_modules" + } +} +# tftest modules=1 resources=2 files=custom-module-sha-1 inventory=custom-modules-sha.yaml +``` + +```yaml +# tftest-file id=custom-module-sha-1 path=data/scc_sha_custom_modules/cloudkmKeyRotationPeriod.yaml schema=scc-sha-custom-modules.schema.json +cloudkmKeyRotationPeriod: + description: "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation: "Set the rotation period to at most 30 days." + severity: "MEDIUM" + predicate: + expression: "resource.rotationPeriod > duration(\"2592000s\")" + resource_selector: + resource_types: + - "cloudkms.googleapis.com/CryptoKey" +``` + ## Tags + Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage. ```hcl @@ -413,9 +472,11 @@ module "folder" { | [main.tf](./main.tf) | Module-level locals and resources. | google_assured_workloads_workload · google_compute_firewall_policy_association · google_essential_contacts_contact · google_folder | | [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | +| [scc-sha-custom-modules.tf](./scc-sha-custom-modules.tf) | Folder-level Custom modules with Security Health Analytics. | google_scc_management_folder_security_health_analytics_custom_module | | [tags.tf](./tags.tf) | None | google_tags_tag_binding | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-logging.tf](./variables-logging.tf) | None | | +| [variables-scc.tf](./variables-scc.tf) | None | | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | @@ -427,23 +488,24 @@ module "folder" { | [contacts](variables.tf#L70) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [context](variables.tf#L78) | Context-specific interpolations. | object({…}) | | {} | | [deletion_protection](variables.tf#L91) | Deletion protection setting for this folder. | bool | | false | -| [factories_config](variables.tf#L97) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L106) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | -| [folder_create](variables.tf#L115) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [factories_config](variables.tf#L97) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L107) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | +| [folder_create](variables.tf#L116) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | | [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | -| [id](variables.tf#L121) | Folder ID in case you use folder_create=false. | string | | null | +| [id](variables.tf#L122) | Folder ID in case you use folder_create=false. | string | | null | | [logging_data_access](variables-logging.tf#L17) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-logging.tf#L28) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L45) | Logging sinks to create for the folder. | map(object({…})) | | {} | -| [name](variables.tf#L127) | Folder name. | string | | null | -| [org_policies](variables.tf#L133) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | -| [parent](variables.tf#L161) | Parent in folders/folder_id or organizations/org_id format. | string | | null | -| [tag_bindings](variables.tf#L175) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [name](variables.tf#L128) | Folder name. | string | | null | +| [org_policies](variables.tf#L134) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L162) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L176) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs @@ -453,5 +515,7 @@ module "folder" { | [folder](outputs.tf#L22) | Folder resource. | | | [id](outputs.tf#L27) | Fully qualified folder id. | | | [name](outputs.tf#L38) | Folder name. | | -| [sink_writer_identities](outputs.tf#L47) | Writer identities created for each sink. | | +| [organization_policies_ids](outputs.tf#L47) | Map of ORGANIZATION_POLICIES => ID in the folder. | | +| [scc_custom_sha_modules_ids](outputs.tf#L52) | Map of SCC CUSTOM SHA MODULES => ID in the folder. | | +| [sink_writer_identities](outputs.tf#L57) | Writer identities created for each sink. | | diff --git a/modules/folder/outputs.tf b/modules/folder/outputs.tf index f9acdad40..58693e74f 100644 --- a/modules/folder/outputs.tf +++ b/modules/folder/outputs.tf @@ -44,6 +44,16 @@ output "name" { ) } +output "organization_policies_ids" { + description = "Map of ORGANIZATION_POLICIES => ID in the folder." + value = { for k, v in google_org_policy_policy.default : k => v.id } +} + +output "scc_custom_sha_modules_ids" { + description = "Map of SCC CUSTOM SHA MODULES => ID in the folder." + value = { for k, v in google_scc_management_folder_security_health_analytics_custom_module.scc_folder_custom_module : k => v.id } +} + output "sink_writer_identities" { description = "Writer identities created for each sink." value = { diff --git a/modules/folder/scc-sha-custom-modules.tf b/modules/folder/scc-sha-custom-modules.tf new file mode 100644 index 000000000..12d7a30eb --- /dev/null +++ b/modules/folder/scc-sha-custom-modules.tf @@ -0,0 +1,69 @@ +/** + * Copyright 2025 Google LLC + * + * 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. + */ + +# tfdoc:file:description Folder-level Custom modules with Security Health Analytics. + +locals { + _scc_sha_custom_modules_factory_path = pathexpand(coalesce(var.factories_config.scc_sha_custom_modules, "-")) + _scc_sha_custom_modules_factory_data_raw = merge([ + for f in try(fileset(local._scc_sha_custom_modules_factory_path, "*.yaml"), []) : + yamldecode(file("${local._scc_sha_custom_modules_factory_path}/${f}")) + ]...) + _scc_sha_custom_modules_factory_data = { + for k, v in local._scc_sha_custom_modules_factory_data_raw : + k => { + description = try(v.description, null) + severity = v.severity + recommendation = v.recommendation + predicate = v.predicate + resource_selector = v.resource_selector + enablement_state = try(v.enablement_state, "ENABLED") + } + } + _scc_sha_custom_modules = merge( + local._scc_sha_custom_modules_factory_data, + var.scc_sha_custom_modules + ) + scc_sha_custom_modules = { + for k, v in local._scc_sha_custom_modules : + k => merge(v, { + name = k + parent = local.folder_id + }) + } + +} + +resource "google_scc_management_folder_security_health_analytics_custom_module" "scc_folder_custom_module" { + provider = google + + for_each = local.scc_sha_custom_modules + folder = replace(local.folder_id, "folders/", "") + location = "global" + display_name = each.value.name + custom_config { + predicate { + expression = each.value.predicate.expression + } + resource_selector { + resource_types = each.value.resource_selector.resource_types + } + description = each.value.description + recommendation = each.value.recommendation + severity = each.value.severity + } + enablement_state = each.value.enablement_state +} diff --git a/modules/folder/schemas/scc-sha-custom-modules.schema.json b/modules/folder/schemas/scc-sha-custom-modules.schema.json new file mode 100644 index 000000000..2f0794b6f --- /dev/null +++ b/modules/folder/schemas/scc-sha-custom-modules.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SCC Security Health Analytics Custom Modules", + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "type": "object", + "required": [ + "predicate", + "resource_selector", + "severity" + ], + "properties": { + "description": { + "type": "string" + }, + "predicate": { + "type": "object", + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + } + } + }, + "recommendation": { + "type": "string" + }, + "resource_selector": { + "type": "object", + "required": [ + "resource_types" + ], + "properties": { + "resource_types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "severity": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/modules/folder/schemas/scc-sha-custom-modules.schema.md b/modules/folder/schemas/scc-sha-custom-modules.schema.md new file mode 100644 index 000000000..f071c9558 --- /dev/null +++ b/modules/folder/schemas/scc-sha-custom-modules.schema.md @@ -0,0 +1,19 @@ +# SCC Security Health Analytics Custom Modules + + + +## Properties + +- **`^[a-zA-Z]+$`**: *object* + - **description**: *string* + - ⁺**predicate**: *object* + - ⁺**expression**: *string* + - **recommendation**: *string* + - ⁺**resource_selector**: *object* + - ⁺**resource_types**: *array* + - items: *string* + - ⁺**severity**: *string* + +## Definitions + + diff --git a/modules/folder/variables-scc.tf b/modules/folder/variables-scc.tf new file mode 100644 index 000000000..115d7967f --- /dev/null +++ b/modules/folder/variables-scc.tf @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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. + */ + +variable "scc_sha_custom_modules" { + description = "SCC custom modules keyed by module name." + type = map(object({ + description = optional(string) + severity = string + recommendation = string + predicate = object({ + expression = string + }) + resource_selector = object({ + resource_types = list(string) + }) + enablement_state = optional(string, "ENABLED") + })) + default = {} + nullable = false +} diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 94084f243..b6b675a1b 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -97,7 +97,8 @@ variable "deletion_protection" { variable "factories_config" { description = "Paths to data files and folders that enable factory functionality." type = object({ - org_policies = optional(string) + org_policies = optional(string) + scc_sha_custom_modules = optional(string) }) nullable = false default = {} @@ -176,4 +177,4 @@ variable "tag_bindings" { description = "Tag bindings for this folder, in key => tag value id format." type = map(string) default = null -} +} \ No newline at end of file diff --git a/modules/organization/README.md b/modules/organization/README.md index 35b96726f..cb44aec9f 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -7,6 +7,7 @@ This module allows managing several organization properties: - audit logging configuration for services - organization policies - organization policy custom constraints +- Security Command Center custom modules To manage organization policies, the `orgpolicy.googleapis.com` service should be enabled in the quota project. @@ -25,6 +26,8 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b - [Data Access Logs](#data-access-logs) - [Custom Roles](#custom-roles) - [Custom Roles Factory](#custom-roles-factory) +- [Custom Security Health Analytics Modules](#custom-security-health-analytics-modules) + - [Custom Security Health Analytics Modules Factory](#custom-security-health-analytics-modules-factory) - [Tags](#tags) - [Tags Factory](#tags-factory) - [Files](#files) @@ -429,8 +432,64 @@ includedPermissions: - resourcemanager.projects.list ``` +## Custom Security Health Analytics Modules + +[Security Health Analytics custom modules](https://cloud.google.com/security-command-center/docs/custom-modules-sha-create) can be defined via the `scc_sha_custom_modules` variable: + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + scc_sha_custom_modules = { + cloudkmKeyRotationPeriod = { + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + predicate = { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector = { + resource_types = ["cloudkms.googleapis.com/CryptoKey"] + } + } + } +} +# tftest modules=1 resources=1 inventory=custom-modules-sha.yaml +``` + +### Custom Security Health Analytics Modules Factory + +Custom modules can also be specified via a factory. Each file is mapped to a custom module, where the module name defaults to the file name. + +Custom modules defined via the variable are merged with those coming from the factory, and override them in case of duplicate names. + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + factories_config = { + scc_sha_custom_modules = "data/scc_sha_custom_modules" + } +} +# tftest modules=1 resources=1 files=custom-module-sha-1 inventory=custom-modules-sha.yaml +``` + +```yaml +# tftest-file id=custom-module-sha-1 path=data/scc_sha_custom_modules/cloudkmKeyRotationPeriod.yaml schema=scc-sha-custom-modules.schema.json +cloudkmKeyRotationPeriod: + description: "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation: "Set the rotation period to at most 30 days." + severity: "MEDIUM" + predicate: + expression: "resource.rotationPeriod > duration(\"2592000s\")" + resource_selector: + resource_types: + - "cloudkms.googleapis.com/CryptoKey" +``` + ## Tags + Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage. ```hcl @@ -594,9 +653,11 @@ values: | [org-policy-custom-constraints.tf](./org-policy-custom-constraints.tf) | None | google_org_policy_custom_constraint | | [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | +| [scc-sha-custom-modules.tf](./scc-sha-custom-modules.tf) | Organization-level Custom modules with Security Health Analytics. | google_scc_management_organization_security_health_analytics_custom_module | | [tags.tf](./tags.tf) | Manages GCP Secure Tags, keys, values, and IAM. | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_key_iam_member · google_tags_tag_value · google_tags_tag_value_iam_binding · google_tags_tag_value_iam_member | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-logging.tf](./variables-logging.tf) | None | | +| [variables-scc.tf](./variables-scc.tf) | None | | | [variables-tags.tf](./variables-tags.tf) | None | | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | @@ -605,12 +666,12 @@ values: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L113) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L114) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [contacts](variables.tf#L17) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | | [context](variables.tf#L24) | Context-specific interpolations. | object({…}) | | {} | | [custom_roles](variables.tf#L43) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [factories_config](variables.tf#L50) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L62) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | +| [factories_config](variables.tf#L50) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L63) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | | [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | @@ -621,8 +682,9 @@ values: | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L45) | Logging sinks to create for the organization. | map(object({…})) | | {} | | [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | -| [org_policies](variables.tf#L71) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policy_custom_constraints](variables.tf#L99) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L72) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L100) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | | [tag_bindings](variables-tags.tf#L82) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | | [tags](variables-tags.tf#L89) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [tags_config](variables-tags.tf#L154) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | @@ -638,7 +700,9 @@ values: | [network_tag_keys](outputs.tf#L50) | Tag key resources. | | | [network_tag_values](outputs.tf#L59) | Tag value resources. | | | [organization_id](outputs.tf#L69) | Organization id dependent on module resources. | | -| [sink_writer_identities](outputs.tf#L86) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L94) | Tag key resources. | | -| [tag_values](outputs.tf#L103) | Tag value resources. | | +| [organization_policies_ids](outputs.tf#L86) | Map of ORGANIZATION_POLICIES => ID in the organization. | | +| [scc_custom_sha_modules_ids](outputs.tf#L91) | Map of SCC CUSTOM SHA MODULES => ID in the organization. | | +| [sink_writer_identities](outputs.tf#L96) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L104) | Tag key resources. | | +| [tag_values](outputs.tf#L113) | Tag value resources. | | diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index 76c335ddf..43c8062eb 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -83,6 +83,16 @@ output "organization_id" { ] } +output "organization_policies_ids" { + description = "Map of ORGANIZATION_POLICIES => ID in the organization." + value = { for k, v in google_org_policy_policy.default : k => v.id } +} + +output "scc_custom_sha_modules_ids" { + description = "Map of SCC CUSTOM SHA MODULES => ID in the organization." + value = { for k, v in google_scc_management_organization_security_health_analytics_custom_module.scc_organization_custom_module : k => v.id } +} + output "sink_writer_identities" { description = "Writer identities created for each sink." value = { @@ -107,3 +117,4 @@ output "tag_values" { k => v if local.tag_values[k].tag_network == null } } + diff --git a/modules/organization/scc-sha-custom-modules.tf b/modules/organization/scc-sha-custom-modules.tf new file mode 100644 index 000000000..e6c336e29 --- /dev/null +++ b/modules/organization/scc-sha-custom-modules.tf @@ -0,0 +1,68 @@ +/** + * Copyright 2025 Google LLC + * + * 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. + */ + +# tfdoc:file:description Organization-level Custom modules with Security Health Analytics. + +locals { + _scc_sha_custom_modules_factory_path = pathexpand(coalesce(var.factories_config.scc_sha_custom_modules, "-")) + _scc_sha_custom_modules_factory_data_raw = merge([ + for f in try(fileset(local._scc_sha_custom_modules_factory_path, "*.yaml"), []) : + yamldecode(file("${local._scc_sha_custom_modules_factory_path}/${f}")) + ]...) + _scc_sha_custom_modules_factory_data = { + for k, v in local._scc_sha_custom_modules_factory_data_raw : + k => { + description = try(v.description, null) + severity = v.severity + recommendation = v.recommendation + predicate = v.predicate + resource_selector = v.resource_selector + enablement_state = try(v.enablement_state, "ENABLED") + } + } + _scc_sha_custom_modules = merge( + local._scc_sha_custom_modules_factory_data, + var.scc_sha_custom_modules + ) + scc_sha_custom_modules = { + for k, v in local._scc_sha_custom_modules : + k => merge(v, { + name = k + parent = var.organization_id + }) + } +} + +resource "google_scc_management_organization_security_health_analytics_custom_module" "scc_organization_custom_module" { + provider = google + + for_each = local.scc_sha_custom_modules + organization = replace(var.organization_id, "organizations/", "") + location = "global" + display_name = each.value.name + custom_config { + predicate { + expression = each.value.predicate.expression + } + resource_selector { + resource_types = each.value.resource_selector.resource_types + } + description = each.value.description + recommendation = each.value.recommendation + severity = each.value.severity + } + enablement_state = each.value.enablement_state +} \ No newline at end of file diff --git a/modules/organization/schemas/scc-sha-custom-modules.schema.json b/modules/organization/schemas/scc-sha-custom-modules.schema.json new file mode 100644 index 000000000..2f0794b6f --- /dev/null +++ b/modules/organization/schemas/scc-sha-custom-modules.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SCC Security Health Analytics Custom Modules", + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "type": "object", + "required": [ + "predicate", + "resource_selector", + "severity" + ], + "properties": { + "description": { + "type": "string" + }, + "predicate": { + "type": "object", + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + } + } + }, + "recommendation": { + "type": "string" + }, + "resource_selector": { + "type": "object", + "required": [ + "resource_types" + ], + "properties": { + "resource_types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "severity": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/modules/organization/schemas/scc-sha-custom-modules.schema.md b/modules/organization/schemas/scc-sha-custom-modules.schema.md new file mode 100644 index 000000000..f071c9558 --- /dev/null +++ b/modules/organization/schemas/scc-sha-custom-modules.schema.md @@ -0,0 +1,19 @@ +# SCC Security Health Analytics Custom Modules + + + +## Properties + +- **`^[a-zA-Z]+$`**: *object* + - **description**: *string* + - ⁺**predicate**: *object* + - ⁺**expression**: *string* + - **recommendation**: *string* + - ⁺**resource_selector**: *object* + - ⁺**resource_types**: *array* + - items: *string* + - ⁺**severity**: *string* + +## Definitions + + diff --git a/modules/organization/variables-scc.tf b/modules/organization/variables-scc.tf new file mode 100644 index 000000000..115d7967f --- /dev/null +++ b/modules/organization/variables-scc.tf @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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. + */ + +variable "scc_sha_custom_modules" { + description = "SCC custom modules keyed by module name." + type = map(object({ + description = optional(string) + severity = string + recommendation = string + predicate = object({ + expression = string + }) + resource_selector = object({ + resource_types = list(string) + }) + enablement_state = optional(string, "ENABLED") + })) + default = {} + nullable = false +} diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 110417d6c..c10b2e59f 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -53,6 +53,7 @@ variable "factories_config" { custom_roles = optional(string) org_policies = optional(string) org_policy_custom_constraints = optional(string) + scc_sha_custom_modules = optional(string) tags = optional(string) }) nullable = false diff --git a/modules/project/README.md b/modules/project/README.md index 02cd0334c..3f5927d9c 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -20,6 +20,8 @@ This module implements the creation and management of one GCP project including - [Data Access Logs](#data-access-logs) - [Log Scopes](#log-scopes) - [Cloud KMS Encryption Keys](#cloud-kms-encryption-keys) +- [Custom Security Health Analytics Modules](#custom-security-health-analytics-modules) + - [Custom Security Health Analytics Modules Factory](#custom-security-health-analytics-modules-factory) - [Tags](#tags) - [Tags Factory](#tags-factory) - [Tag Bindings](#tag-bindings) @@ -938,7 +940,69 @@ module "project" { } ``` +## Custom Security Health Analytics Modules + +[Security Health Analytics custom modules](https://cloud.google.com/security-command-center/docs/custom-modules-sha-create) can be defined via the `scc_sha_custom_modules` variable: + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "project" + prefix = var.prefix + parent = var.folder_id + scc_sha_custom_modules = { + cloudkmKeyRotationPeriod = { + description = "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation = "Set the rotation period to at most 30 days." + severity = "MEDIUM" + predicate = { + expression = "resource.rotationPeriod > duration(\"2592000s\")" + } + resource_selector = { + resource_types = ["cloudkms.googleapis.com/CryptoKey"] + } + } + } +} +# tftest modules=1 resources=2 inventory=custom-modules-sha.yaml +``` + +### Custom Security Health Analytics Modules Factory + +Custom modules can also be specified via a factory. Each file is mapped to a custom module, where the module name defaults to the file name. + +Custom modules defined via the variable are merged with those coming from the factory, and override them in case of duplicate names. + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "project" + prefix = var.prefix + parent = var.folder_id + factories_config = { + scc_sha_custom_modules = "data/scc_sha_custom_modules" + } +} +# tftest modules=1 resources=2 files=custom-module-sha-1 inventory=custom-modules-sha.yaml +``` + +```yaml +# tftest-file id=custom-module-sha-1 path=data/scc_sha_custom_modules/cloudkmKeyRotationPeriod.yaml schema=scc-sha-custom-modules.schema.json +cloudkmKeyRotationPeriod: + description: "The rotation period of the identified cryptokey resource exceeds 30 days." + recommendation: "Set the rotation period to at most 30 days." + severity: "MEDIUM" + predicate: + expression: "resource.rotationPeriod > duration(\"2592000s\")" + resource_selector: + resource_types: + - "cloudkms.googleapis.com/CryptoKey" +``` + ## Tags +``` Refer to the [Creating and managing tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) documentation for details on usage. @@ -1785,12 +1849,14 @@ alerts: | [organization-policies.tf](./organization-policies.tf) | Project-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [quotas.tf](./quotas.tf) | None | google_cloud_quotas_quota_preference | +| [scc-sha-custom-modules.tf](./scc-sha-custom-modules.tf) | Project-level Custom modules with Security Health Analytics. | google_scc_management_project_security_health_analytics_custom_module | | [service-agents.tf](./service-agents.tf) | Service agents supporting resources. | google_project_default_service_accounts · google_project_iam_member · google_project_service_identity | | [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | google_compute_shared_vpc_host_project · google_compute_shared_vpc_service_project · google_compute_subnetwork_iam_member · google_project_iam_member | | [tags.tf](./tags.tf) | Manages GCP Secure Tags, keys, values, and IAM. | google_tags_tag_binding · google_tags_tag_key · google_tags_tag_key_iam_binding · google_tags_tag_key_iam_member · google_tags_tag_value · google_tags_tag_value_iam_binding · google_tags_tag_value_iam_member | | [variables-iam.tf](./variables-iam.tf) | None | | | [variables-observability.tf](./variables-observability.tf) | None | | | [variables-quotas.tf](./variables-quotas.tf) | None | | +| [variables-scc.tf](./variables-scc.tf) | None | | | [variables-tags.tf](./variables-tags.tf) | None | | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | @@ -1800,26 +1866,26 @@ alerts: | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L130) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L132) | Project name and id suffix. | string | ✓ | | | [alerts](variables-observability.tf#L17) | Monitoring alerts. | map(object({…})) | | {} | | [auto_create_network](variables.tf#L17) | Whether to create the default network for the project. | bool | | false | | [billing_account](variables.tf#L23) | Billing account id. | string | | null | | [compute_metadata](variables.tf#L29) | Optional compute metadata key/values. Only usable if compute API has been enabled. | map(string) | | {} | | [contacts](variables.tf#L36) | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES. | map(list(string)) | | {} | -| [context](variables.tf#L43) | Context-specific interpolations. | object({…}) | | {} | -| [custom_roles](variables.tf#L61) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [default_network_tier](variables.tf#L68) | Default compute network tier for the project. | string | | null | -| [default_service_account](variables.tf#L74) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | -| [deletion_policy](variables.tf#L87) | Deletion policy setting for this project. | string | | "DELETE" | -| [descriptive_name](variables.tf#L98) | Name of the project name. Used for project name instead of `name` variable. | string | | null | -| [factories_config](variables.tf#L104) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [context](variables.tf#L43) | Context-specific interpolations. | object({…}) | | {} | +| [custom_roles](variables.tf#L62) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | +| [default_network_tier](variables.tf#L69) | Default compute network tier for the project. | string | | null | +| [default_service_account](variables.tf#L75) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | +| [deletion_policy](variables.tf#L88) | Deletion policy setting for this project. | string | | "DELETE" | +| [descriptive_name](variables.tf#L99) | Name of the project name. Used for project name instead of `name` variable. | string | | null | +| [factories_config](variables.tf#L105) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | | [iam](variables-iam.tf#L17) | Authoritative IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | -| [labels](variables.tf#L117) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L124) | If non-empty, creates a project lien with this description. | string | | null | +| [labels](variables.tf#L119) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L126) | If non-empty, creates a project lien with this description. | string | | null | | [log_scopes](variables-observability.tf#L117) | Log scopes under this project. | map(object({…})) | | {} | | [logging_data_access](variables-observability.tf#L127) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-observability.tf#L138) | Logging exclusions for this project in the form {NAME -> FILTER}. | map(string) | | {} | @@ -1828,23 +1894,24 @@ alerts: | [metric_scopes](variables-observability.tf#L216) | List of projects that will act as metric scopes for this project. | list(string) | | [] | | [network_tags](variables-tags.tf#L17) | Network tags by key name. If `id` is provided, key creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [notification_channels](variables-observability.tf#L223) | Monitoring notification channels. | map(object({…})) | | {} | -| [org_policies](variables.tf#L135) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | -| [parent](variables.tf#L163) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L177) | Optional prefix used to generate project id and name. | string | | null | -| [project_reuse](variables.tf#L187) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | +| [org_policies](variables.tf#L137) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [parent](variables.tf#L165) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L179) | Optional prefix used to generate project id and name. | string | | null | +| [project_reuse](variables.tf#L189) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | | [quotas](variables-quotas.tf#L17) | Service quota configuration. | map(object({…})) | | {} | -| [service_agents_config](variables.tf#L207) | Automatic service agent configuration options. | object({…}) | | {} | -| [service_config](variables.tf#L217) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L229) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | -| [services](variables.tf#L236) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L242) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L252) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L289) | Deprecated. Use deletion_policy. | bool | | null | +| [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | +| [service_agents_config](variables.tf#L209) | Automatic service agent configuration options. | object({…}) | | {} | +| [service_config](variables.tf#L219) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L231) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | +| [services](variables.tf#L238) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L244) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L254) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L291) | Deprecated. Use deletion_policy. | bool | | null | | [tag_bindings](variables-tags.tf#L82) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L89) | Tags by key name. If `id` is provided, key or value creation is skipped. The `iam` attribute behaves like the similarly named one at module level. | map(object({…})) | | {} | | [tags_config](variables-tags.tf#L154) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | -| [universe](variables.tf#L301) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | -| [vpc_sc](variables.tf#L311) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | +| [universe](variables.tf#L303) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [vpc_sc](variables.tf#L313) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | ## Outputs @@ -1861,12 +1928,14 @@ alerts: | [notification_channel_names](outputs.tf#L90) | Notification channel names. | | | [notification_channels](outputs.tf#L98) | Full notification channel objects. | | | [number](outputs.tf#L103) | Project number. | | -| [project_id](outputs.tf#L121) | Project id. | | -| [quota_configs](outputs.tf#L139) | Quota configurations. | | -| [quotas](outputs.tf#L150) | Quota resources. | | -| [service_agents](outputs.tf#L155) | List of all (active) service agents for this project. | | -| [services](outputs.tf#L164) | Service APIs to enable in the project. | | -| [sink_writer_identities](outputs.tf#L173) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L180) | Tag key resources. | | -| [tag_values](outputs.tf#L189) | Tag value resources. | | +| [organization_policies_ids](outputs.tf#L118) | Map of ORGANIZATION_POLICIES => ID in the organization. | | +| [project_id](outputs.tf#L125) | Project id. | | +| [quota_configs](outputs.tf#L143) | Quota configurations. | | +| [quotas](outputs.tf#L154) | Quota resources. | | +| [scc_custom_sha_modules_ids](outputs.tf#L159) | Map of SCC CUSTOM SHA MODULES => ID in the project. | | +| [service_agents](outputs.tf#L164) | List of all (active) service agents for this project. | | +| [services](outputs.tf#L173) | Service APIs to enable in the project. | | +| [sink_writer_identities](outputs.tf#L182) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L189) | Tag key resources. | | +| [tag_values](outputs.tf#L198) | Tag value resources. | | diff --git a/modules/project/alerts.tf b/modules/project/alerts.tf index 7418c655d..72b5122e1 100644 --- a/modules/project/alerts.tf +++ b/modules/project/alerts.tf @@ -87,12 +87,14 @@ locals { evaluation_missing_data = try(c.condition_threshold.evaluation_missing_data, null) filter = try(c.condition_threshold.filter, null) threshold_value = try(c.condition_threshold.threshold_value, null) - aggregations = !can(c.condition_threshold.aggregations) ? null : { - per_series_aligner = try(c.condition_threshold.aggregations.per_series_aligner, null) - group_by_fields = try(c.condition_threshold.aggregations.group_by_fields, null) - cross_series_reducer = try(c.condition_threshold.aggregations.cross_series_reducer, null) - alignment_period = try(c.condition_threshold.aggregations.alignment_period, null) - } + aggregations = !can(c.condition_threshold.aggregations) ? null : [ + for a in c.condition_threshold.aggregations : { + per_series_aligner = try(a.per_series_aligner, null) + group_by_fields = try(a.group_by_fields, null) + cross_series_reducer = try(a.cross_series_reducer, null) + alignment_period = try(a.alignment_period, null) + } + ] denominator_aggregations = !can(c.condition_threshold.denominator_aggregations) ? null : { per_series_aligner = try(c.condition_threshold.denominator_aggregations.per_series_aligner, null) group_by_fields = try(c.condition_threshold.denominator_aggregations.group_by_fields, null) @@ -282,4 +284,6 @@ resource "google_monitoring_alert_policy" "alerts" { } } } + + depends_on = [google_logging_metric.metrics] } diff --git a/modules/project/logging-metrics.tf b/modules/project/logging-metrics.tf index f46558690..03673a580 100644 --- a/modules/project/logging-metrics.tf +++ b/modules/project/logging-metrics.tf @@ -63,13 +63,18 @@ locals { } resource "google_logging_metric" "metrics" { - for_each = local.metrics - project = local.project.project_id - name = each.key - filter = each.value.filter - description = each.value.description - disabled = each.value.disabled - bucket_name = each.value.bucket_name + for_each = local.metrics + project = local.project.project_id + name = each.key + filter = each.value.filter + description = each.value.description + disabled = each.value.disabled + bucket_name = try( + # first try to check the context + var.context.logging_bucket_names[each.value.bucket_name], + # if nothing else, use the provided channel as is + each.value.bucket_name + ) value_extractor = each.value.value_extractor label_extractors = each.value.label_extractors diff --git a/modules/project/outputs.tf b/modules/project/outputs.tf index d717cc8e2..dcc35c57f 100644 --- a/modules/project/outputs.tf +++ b/modules/project/outputs.tf @@ -115,6 +115,10 @@ output "number" { google_project_iam_member.service_agents ] } +output "organization_policies_ids" { + description = "Map of ORGANIZATION_POLICIES => ID in the organization." + value = { for k, v in google_org_policy_policy.default : k => v.id } +} # TODO: deprecate in favor of id @@ -152,6 +156,11 @@ output "quotas" { value = google_cloud_quotas_quota_preference.default } +output "scc_custom_sha_modules_ids" { + description = "Map of SCC CUSTOM SHA MODULES => ID in the project." + value = { for k, v in google_scc_management_project_security_health_analytics_custom_module.scc_project_custom_module : k => v.id } +} + output "service_agents" { description = "List of all (active) service agents for this project." value = local.aliased_service_agents diff --git a/modules/project/scc-sha-custom-modules.tf b/modules/project/scc-sha-custom-modules.tf new file mode 100644 index 000000000..04276b4bd --- /dev/null +++ b/modules/project/scc-sha-custom-modules.tf @@ -0,0 +1,68 @@ +/** + * Copyright 2025 Google LLC + * + * 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. + */ + +# tfdoc:file:description Project-level Custom modules with Security Health Analytics. + +locals { + _scc_sha_custom_modules_factory_path = pathexpand(coalesce(var.factories_config.scc_sha_custom_modules, "-")) + _scc_sha_custom_modules_factory_data_raw = merge([ + for f in try(fileset(local._scc_sha_custom_modules_factory_path, "*.yaml"), []) : + yamldecode(file("${local._scc_sha_custom_modules_factory_path}/${f}")) + ]...) + _scc_sha_custom_modules_factory_data = { + for k, v in local._scc_sha_custom_modules_factory_data_raw : + k => { + description = try(v.description, null) + severity = v.severity + recommendation = v.recommendation + predicate = v.predicate + resource_selector = v.resource_selector + enablement_state = try(v.enablement_state, "ENABLED") + } + } + _scc_sha_custom_modules = merge( + local._scc_sha_custom_modules_factory_data, + var.scc_sha_custom_modules + ) + scc_sha_custom_modules = { + for k, v in local._scc_sha_custom_modules : + k => merge(v, { + name = k + parent = "projects/${local.project.project_id}" + }) + } +} + +resource "google_scc_management_project_security_health_analytics_custom_module" "scc_project_custom_module" { + provider = google + + for_each = local.scc_sha_custom_modules + project = local.project.project_id + location = "global" + display_name = each.value.name + custom_config { + predicate { + expression = each.value.predicate.expression + } + resource_selector { + resource_types = each.value.resource_selector.resource_types + } + description = each.value.description + recommendation = each.value.recommendation + severity = each.value.severity + } + enablement_state = each.value.enablement_state +} diff --git a/modules/project/schemas/scc-sha-custom-modules.schema.json b/modules/project/schemas/scc-sha-custom-modules.schema.json new file mode 100644 index 000000000..2f0794b6f --- /dev/null +++ b/modules/project/schemas/scc-sha-custom-modules.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SCC Security Health Analytics Custom Modules", + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "type": "object", + "required": [ + "predicate", + "resource_selector", + "severity" + ], + "properties": { + "description": { + "type": "string" + }, + "predicate": { + "type": "object", + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + } + } + }, + "recommendation": { + "type": "string" + }, + "resource_selector": { + "type": "object", + "required": [ + "resource_types" + ], + "properties": { + "resource_types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "severity": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/modules/project/schemas/scc-sha-custom-modules.schema.md b/modules/project/schemas/scc-sha-custom-modules.schema.md new file mode 100644 index 000000000..f071c9558 --- /dev/null +++ b/modules/project/schemas/scc-sha-custom-modules.schema.md @@ -0,0 +1,19 @@ +# SCC Security Health Analytics Custom Modules + + + +## Properties + +- **`^[a-zA-Z]+$`**: *object* + - **description**: *string* + - ⁺**predicate**: *object* + - ⁺**expression**: *string* + - **recommendation**: *string* + - ⁺**resource_selector**: *object* + - ⁺**resource_types**: *array* + - items: *string* + - ⁺**severity**: *string* + +## Definitions + + diff --git a/modules/project/variables-scc.tf b/modules/project/variables-scc.tf new file mode 100644 index 000000000..115d7967f --- /dev/null +++ b/modules/project/variables-scc.tf @@ -0,0 +1,33 @@ +/** + * Copyright 2025 Google LLC + * + * 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. + */ + +variable "scc_sha_custom_modules" { + description = "SCC custom modules keyed by module name." + type = map(object({ + description = optional(string) + severity = string + recommendation = string + predicate = object({ + expression = string + }) + resource_selector = object({ + resource_types = list(string) + }) + enablement_state = optional(string, "ENABLED") + })) + default = {} + nullable = false +} diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 75fdfa304..0d14a1f76 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -49,6 +49,7 @@ variable "context" { kms_keys = optional(map(string), {}) iam_principals = optional(map(string), {}) notification_channels = optional(map(string), {}) + logging_bucket_names = optional(map(string), {}) project_ids = optional(map(string), {}) tag_keys = optional(map(string), {}) tag_values = optional(map(string), {}) @@ -104,11 +105,12 @@ variable "descriptive_name" { variable "factories_config" { description = "Paths to data files and folders that enable factory functionality." type = object({ - custom_roles = optional(string) - observability = optional(string) - org_policies = optional(string) - quotas = optional(string) - tags = optional(string) + custom_roles = optional(string) + observability = optional(string) + org_policies = optional(string) + quotas = optional(string) + scc_sha_custom_modules = optional(string) + tags = optional(string) }) nullable = false default = {} diff --git a/tests/modules/folder/examples/custom-modules-sha.yaml b/tests/modules/folder/examples/custom-modules-sha.yaml new file mode 100644 index 000000000..0df493518 --- /dev/null +++ b/tests/modules/folder/examples/custom-modules-sha.yaml @@ -0,0 +1,47 @@ +# Copyright 2025 Google LLC +# +# 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. + +values: + module.folder.google_folder.folder[0]: + deletion_protection: false + display_name: Folder name + parent: folders/1122334455 + tags: null + timeouts: null + module.folder.google_scc_management_folder_security_health_analytics_custom_module.scc_folder_custom_module["cloudkmKeyRotationPeriod"]: + custom_config: + - custom_output: [] + description: The rotation period of the identified cryptokey resource exceeds + 30 days. + predicate: + - description: null + expression: resource.rotationPeriod > duration("2592000s") + location: null + title: null + recommendation: Set the rotation period to at most 30 days. + resource_selector: + - resource_types: + - cloudkms.googleapis.com/CryptoKey + severity: MEDIUM + display_name: cloudkmKeyRotationPeriod + enablement_state: ENABLED + location: global + timeouts: null + +counts: + google_folder: 1 + google_scc_management_folder_security_health_analytics_custom_module: 1 + modules: 1 + resources: 2 + diff --git a/tests/modules/organization/examples/custom-modules-sha.yaml b/tests/modules/organization/examples/custom-modules-sha.yaml new file mode 100644 index 000000000..7034ae319 --- /dev/null +++ b/tests/modules/organization/examples/custom-modules-sha.yaml @@ -0,0 +1,40 @@ +# Copyright 2025 Google LLC +# +# 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. + +values: + module.org.google_scc_management_organization_security_health_analytics_custom_module.scc_organization_custom_module["cloudkmKeyRotationPeriod"]: + custom_config: + - custom_output: [] + description: The rotation period of the identified cryptokey resource exceeds + 30 days. + predicate: + - description: null + expression: resource.rotationPeriod > duration("2592000s") + location: null + title: null + recommendation: Set the rotation period to at most 30 days. + resource_selector: + - resource_types: + - cloudkms.googleapis.com/CryptoKey + severity: MEDIUM + display_name: cloudkmKeyRotationPeriod + enablement_state: ENABLED + location: global + organization: '1122334455' + timeouts: null + +counts: + google_scc_management_organization_security_health_analytics_custom_module: 1 + modules: 1 + resources: 1 diff --git a/tests/modules/project/examples/custom-modules-sha.yaml b/tests/modules/project/examples/custom-modules-sha.yaml new file mode 100644 index 000000000..afff35100 --- /dev/null +++ b/tests/modules/project/examples/custom-modules-sha.yaml @@ -0,0 +1,57 @@ +# Copyright 2025 Google LLC +# +# 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. + +values: + module.project.google_project.project[0]: + auto_create_network: false + billing_account: 123456-123456-123456 + deletion_policy: DELETE + effective_labels: + goog-terraform-provisioned: 'true' + folder_id: '1122334455' + labels: null + name: test-project + org_id: null + project_id: test-project + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + module.project.google_scc_management_project_security_health_analytics_custom_module.scc_project_custom_module["cloudkmKeyRotationPeriod"]: + custom_config: + - custom_output: [] + description: The rotation period of the identified cryptokey resource exceeds + 30 days. + predicate: + - description: null + expression: resource.rotationPeriod > duration("2592000s") + location: null + title: null + recommendation: Set the rotation period to at most 30 days. + resource_selector: + - resource_types: + - cloudkms.googleapis.com/CryptoKey + severity: MEDIUM + display_name: cloudkmKeyRotationPeriod + enablement_state: ENABLED + location: global + project: test-project + timeouts: null + +counts: + google_project: 1 + google_scc_management_project_security_health_analytics_custom_module: 1 + modules: 1 + resources: 2 +