diff --git a/modules/folder/README.md b/modules/folder/README.md index c3bcf5c19..c917a982d 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -17,6 +17,8 @@ This module allows the creation and management of folders, including support for - [KMS Autokey](#kms-autokey) - [Custom Security Health Analytics Modules](#custom-security-health-analytics-modules) - [Custom Security Health Analytics Modules Factory](#custom-security-health-analytics-modules-factory) +- [Security Command Center Mute Configs](#security-command-center-mute-configs) + - [Security Command Center Mute Configs Factory](#security-command-center-mute-configs-factory) - [Cloud Asset Inventory Feeds](#cloud-asset-inventory-feeds) - [Tags](#tags) - [Files](#files) @@ -569,6 +571,52 @@ cloudkmKeyRotationPeriod: - "cloudkms.googleapis.com/CryptoKey" ``` +## Security Command Center Mute Configs + +[Security Command Center Mute Configs](https://cloud.google.com/security-command-center/docs/how-to-mute-findings) can be defined via the `scc_mute_configs` variable: + +```hcl +module "folder" { + source = "./fabric/modules/folder" + parent = var.folder_id + name = "Folder name" + scc_mute_configs = { + muteHighSeverity = { + description = "Mute high severity findings" + filter = "severity=\"HIGH\"" + type = "DYNAMIC" + } + } +} +# tftest modules=1 inventory=scc-mute-configs.yaml +``` + +### Security Command Center Mute Configs Factory + +Mute configs can also be specified via a factory. Each file is mapped to a mute config, where the config ID defaults to the file name. + +Mute configs 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_mute_configs = "data/scc_mute_configs" + } +} +# tftest modules=1 files=mute-config-1 inventory=scc-mute-configs.yaml +``` + +```yaml +# tftest-file id=mute-config-1 path=data/scc_mute_configs/muteHighSeverity.yaml schema=scc-mute-config.schema.json +muteHighSeverity: + description: "Mute high severity findings" + filter: "severity=\"HIGH\"" + type: "DYNAMIC" +``` + ## Cloud Asset Inventory Feeds Cloud Asset Inventory feeds allow you to monitor asset changes in real-time by publishing notifications to a Pub/Sub topic. Feeds configured at the folder level will monitor all resources within the folder and its subfolders. @@ -645,6 +693,7 @@ module "folder" { | [organization-policies.tf](./organization-policies.tf) | Folder-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [pam.tf](./pam.tf) | None | google_privileged_access_manager_entitlement | +| [scc-mute-configs.tf](./scc-mute-configs.tf) | Folder-level SCC mute configurations. | google_scc_v2_folder_mute_config | | [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 | | [service-agents.tf](./service-agents.tf) | Service agents supporting resources. | | | [tags.tf](./tags.tf) | None | google_tags_tag_binding | @@ -665,26 +714,27 @@ module "folder" { | [contacts](variables.tf#L122) | 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#L141) | Context-specific interpolations. | object({…}) | | {} | | [deletion_protection](variables.tf#L161) | Deletion protection setting for this folder. | bool | | false | -| [factories_config](variables.tf#L167) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L178) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | -| [folder_create](variables.tf#L187) | Create folder. When set to false, uses id to reference an existing folder. | bool | | true | +| [factories_config](variables.tf#L167) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L179) | Hierarchical firewall policy to associate to this folder. | object({…}) | | null | +| [folder_create](variables.tf#L188) | 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)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | -| [id](variables.tf#L197) | Folder ID in case you use folder_create=false. | string | | null | +| [id](variables.tf#L198) | 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#L203) | Folder name. | string | | null | -| [org_policies](variables.tf#L209) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | +| [name](variables.tf#L204) | Folder name. | string | | null | +| [org_policies](variables.tf#L210) | Organization policies applied to this folder keyed by policy name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | -| [parent](variables.tf#L237) | 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#L251) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | +| [parent](variables.tf#L238) | Parent in folders/folder_id or organizations/org_id format. | string | | null | +| [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | +| [scc_sha_custom_modules](variables-scc.tf#L27) | SCC custom modules keyed by module name. | map(object({…})) | | {} | +| [tag_bindings](variables.tf#L252) | Tag bindings for this folder, in key => tag value id format. | map(string) | | null | ## Outputs diff --git a/modules/folder/scc-mute-configs.tf b/modules/folder/scc-mute-configs.tf new file mode 100644 index 000000000..89e6a6c04 --- /dev/null +++ b/modules/folder/scc-mute-configs.tf @@ -0,0 +1,54 @@ +/** + * 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 SCC mute configurations. + +locals { + _scc_mute_configs_factory_path = pathexpand(coalesce(var.factories_config.scc_mute_configs, "-")) + _scc_mute_configs_factory_data_raw = merge([ + for f in try(fileset(local._scc_mute_configs_factory_path, "*.yaml"), []) : + yamldecode(file("${local._scc_mute_configs_factory_path}/${f}")) + ]...) + _scc_mute_configs_factory_data = { + for k, v in local._scc_mute_configs_factory_data_raw : + k => { + description = try(v.description, null) + filter = v.filter + type = try(v.type, "DYNAMIC") + } + } + _scc_mute_configs = merge( + local._scc_mute_configs_factory_data, + var.scc_mute_configs + ) + scc_mute_configs = { + for k, v in local._scc_mute_configs : + k => merge(v, { + name = k + parent = local.folder_id + }) + } +} + +resource "google_scc_v2_folder_mute_config" "scc_mute_configs" { + for_each = local.scc_mute_configs + folder = replace(local.folder_id, "folders/", "") + location = "global" + mute_config_id = each.key + description = each.value.description + filter = each.value.filter + type = each.value.type +} diff --git a/modules/folder/schemas/scc-mute-config.schema.json b/modules/folder/schemas/scc-mute-config.schema.json new file mode 100644 index 000000000..6a46cb581 --- /dev/null +++ b/modules/folder/schemas/scc-mute-config.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SCC Mute Configurations", + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "type": "object", + "required": [ + "filter" + ], + "properties": { + "description": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "DYNAMIC", + "STATIC" + ], + "default": "DYNAMIC" + } + } + } + } +} diff --git a/modules/folder/schemas/scc-mute-config.schema.md b/modules/folder/schemas/scc-mute-config.schema.md new file mode 100644 index 000000000..15db0d6ce --- /dev/null +++ b/modules/folder/schemas/scc-mute-config.schema.md @@ -0,0 +1,11 @@ +# SCC Mute Configurations + + + +## Properties + +- **`^[a-zA-Z]+$`**: *object* + - **description**: *string* + - ⁺**filter**: *string* + - **type**: *string* + - enum: `DYNAMIC`, `STATIC` diff --git a/modules/folder/variables-scc.tf b/modules/folder/variables-scc.tf index 115d7967f..afa476c8a 100644 --- a/modules/folder/variables-scc.tf +++ b/modules/folder/variables-scc.tf @@ -14,6 +14,16 @@ * limitations under the License. */ +variable "scc_mute_configs" { + description = "SCC mute configurations keyed by name." + type = map(object({ + description = optional(string) + filter = string + type = optional(string, "DYNAMIC") + })) + default = {} + nullable = false +} variable "scc_sha_custom_modules" { description = "SCC custom modules keyed by module name." type = map(object({ diff --git a/modules/folder/variables.tf b/modules/folder/variables.tf index 68114ea4d..0de8855cb 100644 --- a/modules/folder/variables.tf +++ b/modules/folder/variables.tf @@ -169,6 +169,7 @@ variable "factories_config" { type = object({ org_policies = optional(string) pam_entitlements = optional(string) + scc_mute_configs = optional(string) scc_sha_custom_modules = optional(string) }) nullable = false diff --git a/modules/organization/README.md b/modules/organization/README.md index cbabee0fe..7ad46d206 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -31,6 +31,8 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b - [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) +- [Security Command Center Mute Configs](#security-command-center-mute-configs) + - [Security Command Center Mute Configs Factory](#security-command-center-mute-configs-factory) - [Cloud Asset Inventory Feeds](#cloud-asset-inventory-feeds) - [Tags](#tags) - [Tags Factory](#tags-factory) @@ -579,6 +581,50 @@ cloudkmKeyRotationPeriod: - "cloudkms.googleapis.com/CryptoKey" ``` +## Security Command Center Mute Configs + +[Security Command Center Mute Configs](https://cloud.google.com/security-command-center/docs/how-to-mute-findings) can be defined via the `scc_mute_configs` variable: + +```hcl +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + scc_mute_configs = { + muteHighSeverity = { + description = "Mute high severity findings" + filter = "severity=\"HIGH\"" + type = "DYNAMIC" + } + } +} +# tftest modules=1 resources=1 inventory=scc-mute-configs.yaml +``` + +### Security Command Center Mute Configs Factory + +Mute configs can also be specified via a factory. Each file is mapped to a mute config, where the config ID defaults to the file name. + +Mute configs 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_mute_configs = "data/scc_mute_configs" + } +} +# tftest modules=1 resources=1 files=mute-config-1 inventory=scc-mute-configs.yaml +``` + +```yaml +# tftest-file id=mute-config-1 path=data/scc_mute_configs/mute-high-severity.yaml schema=scc-mute-config.schema.json +muteHighSeverity: + description: "Mute high severity findings" + filter: "severity=\"HIGH\"" + type: "DYNAMIC" +``` + ## Cloud Asset Inventory Feeds Cloud Asset Inventory feeds allow you to monitor asset changes in real-time by publishing notifications to a Pub/Sub topic. Feeds configured at the organization level will monitor all resources within the organization. @@ -847,6 +893,7 @@ module "org" { | [organization-policies.tf](./organization-policies.tf) | Organization-level organization policies. | google_org_policy_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [pam.tf](./pam.tf) | None | google_privileged_access_manager_entitlement | +| [scc-mute-configs.tf](./scc-mute-configs.tf) | Organization-level SCC mute configurations. | google_scc_v2_organization_mute_config | | [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 | | [service-agents.tf](./service-agents.tf) | Service agents supporting resources. | | | [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 | @@ -863,13 +910,13 @@ module "org" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [organization_id](variables.tf#L161) | Organization id in organizations/nnnnnn format. | string | ✓ | | +| [organization_id](variables.tf#L162) | Organization id in organizations/nnnnnn format. | string | ✓ | | | [asset_feeds](variables.tf#L18) | Cloud Asset Inventory feeds. | map(object({…})) | | {} | | [contacts](variables.tf#L51) | 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#L69) | Context-specific interpolations. | object({…}) | | {} | | [custom_roles](variables.tf#L89) | Map of role name => list of permissions to create in this project. | map(list(string)) | | {} | -| [factories_config](variables.tf#L96) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | -| [firewall_policy](variables.tf#L110) | Hierarchical firewall policies to associate to the organization. | object({…}) | | null | +| [factories_config](variables.tf#L96) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [firewall_policy](variables.tf#L111) | 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({…})) | | {} | @@ -881,10 +928,11 @@ module "org" { | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | | [logging_sinks](variables-logging.tf#L46) | 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#L119) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | -| [org_policy_custom_constraints](variables.tf#L147) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L120) | Organization policies applied to this organization keyed by policy name. | map(object({…})) | | {} | +| [org_policy_custom_constraints](variables.tf#L148) | Organization policy custom constraints keyed by constraint name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | -| [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | +| [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | +| [scc_sha_custom_modules](variables-scc.tf#L28) | SCC custom modules keyed by module name. | map(object({…})) | | {} | | [tag_bindings](variables-tags.tf#L89) | Tag bindings for this organization, in key => tag value id format. | map(string) | | {} | | [tags](variables-tags.tf#L96) | 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#L161) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | @@ -904,10 +952,11 @@ module "org" { | [organization_id](outputs.tf#L81) | Organization id dependent on module resources. | | | [organization_policies_ids](outputs.tf#L98) | Map of ORGANIZATION_POLICIES => ID in the organization. | | | [scc_custom_sha_modules_ids](outputs.tf#L103) | Map of SCC CUSTOM SHA MODULES => ID in the organization. | | -| [service_agents](outputs.tf#L108) | Identities of all organization-level service agents. | | -| [sink_writer_identities](outputs.tf#L113) | Writer identities created for each sink. | | -| [tag_keys](outputs.tf#L121) | Tag key resources. | | -| [tag_values](outputs.tf#L130) | Tag value resources. | | -| [workforce_identity_provider_names](outputs.tf#L138) | Workforce Identity provider names. | | -| [workforce_identity_providers](outputs.tf#L145) | Workforce Identity provider attributes. | | +| [scc_mute_configs](outputs.tf#L108) | SCC mute configurations. | | +| [service_agents](outputs.tf#L113) | Identities of all organization-level service agents. | | +| [sink_writer_identities](outputs.tf#L118) | Writer identities created for each sink. | | +| [tag_keys](outputs.tf#L126) | Tag key resources. | | +| [tag_values](outputs.tf#L135) | Tag value resources. | | +| [workforce_identity_provider_names](outputs.tf#L143) | Workforce Identity provider names. | | +| [workforce_identity_providers](outputs.tf#L150) | Workforce Identity provider attributes. | | diff --git a/modules/organization/outputs.tf b/modules/organization/outputs.tf index d29c690e3..a77041b2e 100644 --- a/modules/organization/outputs.tf +++ b/modules/organization/outputs.tf @@ -105,6 +105,11 @@ output "scc_custom_sha_modules_ids" { value = { for k, v in google_scc_management_organization_security_health_analytics_custom_module.scc_organization_custom_module : k => v.id } } +output "scc_mute_configs" { + description = "SCC mute configurations." + value = google_scc_v2_organization_mute_config.scc_mute_configs +} + output "service_agents" { description = "Identities of all organization-level service agents." value = local.service_agents diff --git a/modules/organization/scc-mute-configs.tf b/modules/organization/scc-mute-configs.tf new file mode 100644 index 000000000..89e182b57 --- /dev/null +++ b/modules/organization/scc-mute-configs.tf @@ -0,0 +1,60 @@ +/** + * 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 SCC mute configurations. + +locals { + _scc_mute_configs_factory_path = pathexpand(coalesce(var.factories_config.scc_mute_configs, "-")) + _scc_mute_configs_factory_data_raw = merge([ + for f in try(fileset(local._scc_mute_configs_factory_path, "*.yaml"), []) : + yamldecode(file("${local._scc_mute_configs_factory_path}/${f}")) + ]...) + _scc_mute_configs_factory_data = { + for k, v in local._scc_mute_configs_factory_data_raw : + k => { + description = try(v.description, null) + filter = v.filter + type = try(v.type, "DYNAMIC") + } + } + _scc_mute_configs = merge( + local._scc_mute_configs_factory_data, + var.scc_mute_configs + ) + scc_mute_configs = { + for k, v in local._scc_mute_configs : + k => merge(v, { + name = k + parent = var.organization_id + }) + } +} + +resource "google_scc_v2_organization_mute_config" "scc_mute_configs" { + for_each = local.scc_mute_configs + organization = replace(var.organization_id, "organizations/", "") + location = "global" + mute_config_id = each.key + description = each.value.description + filter = each.value.filter + type = each.value.type + + depends_on = [ + google_organization_iam_binding.authoritative, + google_organization_iam_binding.bindings, + google_organization_iam_member.bindings, + ] +} diff --git a/modules/organization/schemas/scc-mute-config.schema.json b/modules/organization/schemas/scc-mute-config.schema.json new file mode 100644 index 000000000..6a46cb581 --- /dev/null +++ b/modules/organization/schemas/scc-mute-config.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SCC Mute Configurations", + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "type": "object", + "required": [ + "filter" + ], + "properties": { + "description": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "DYNAMIC", + "STATIC" + ], + "default": "DYNAMIC" + } + } + } + } +} diff --git a/modules/organization/schemas/scc-mute-config.schema.md b/modules/organization/schemas/scc-mute-config.schema.md new file mode 100644 index 000000000..15db0d6ce --- /dev/null +++ b/modules/organization/schemas/scc-mute-config.schema.md @@ -0,0 +1,11 @@ +# SCC Mute Configurations + + + +## Properties + +- **`^[a-zA-Z]+$`**: *object* + - **description**: *string* + - ⁺**filter**: *string* + - **type**: *string* + - enum: `DYNAMIC`, `STATIC` diff --git a/modules/organization/variables-scc.tf b/modules/organization/variables-scc.tf index 115d7967f..f0a76382b 100644 --- a/modules/organization/variables-scc.tf +++ b/modules/organization/variables-scc.tf @@ -14,6 +14,17 @@ * limitations under the License. */ +variable "scc_mute_configs" { + description = "SCC mute configurations keyed by name." + type = map(object({ + description = optional(string) + filter = string + type = optional(string, "DYNAMIC") + })) + default = {} + nullable = false +} + variable "scc_sha_custom_modules" { description = "SCC custom modules keyed by module name." type = map(object({ diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index fcda66109..ac814cac0 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -100,6 +100,7 @@ variable "factories_config" { org_policies = optional(string) org_policy_custom_constraints = optional(string) pam_entitlements = optional(string) + scc_mute_configs = optional(string) scc_sha_custom_modules = optional(string) tags = optional(string) }) diff --git a/modules/project/README.md b/modules/project/README.md index ebb7a5521..33e9b91a9 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -24,6 +24,8 @@ This module implements the creation and management of one GCP project including - [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) +- [Security Command Center Mute Configs](#security-command-center-mute-configs) + - [Security Command Center Mute Configs Factory](#security-command-center-mute-configs-factory) - [Tags](#tags) - [Tags Factory](#tags-factory) - [Tag Bindings](#tag-bindings) @@ -1029,6 +1031,56 @@ cloudkmKeyRotationPeriod: - "cloudkms.googleapis.com/CryptoKey" ``` +## Security Command Center Mute Configs + +[Security Command Center Mute Configs](https://cloud.google.com/security-command-center/docs/how-to-mute-findings) can be defined via the `scc_mute_configs` variable: + +```hcl +module "project" { + source = "./fabric/modules/project" + billing_account = var.billing_account_id + name = "project" + prefix = var.prefix + parent = var.folder_id + scc_mute_configs = { + muteHighSeverity = { + description = "Mute high severity findings" + filter = "severity=\"HIGH\"" + type = "DYNAMIC" + } + } +} +# tftest modules=1 inventory=scc-mute-configs.yaml +``` + +### Security Command Center Mute Configs Factory + +Mute configs can also be specified via a factory. Each file is mapped to a mute config, where the config ID defaults to the file name. + +Mute configs 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_mute_configs = "data/scc_mute_configs" + } +} +# tftest modules=1 files=mute-config-1 inventory=scc-mute-configs.yaml +``` + +```yaml +# tftest-file id=mute-config-1 path=data/scc_mute_configs/muteHighSeverity.yaml schema=scc-mute-config.schema.json +muteHighSeverity: + description: "Mute high severity findings" + filter: "severity=\"HIGH\"" + type: "DYNAMIC" +``` + ## Tags ``` @@ -2186,6 +2238,7 @@ module "project" { | [outputs.tf](./outputs.tf) | Module outputs. | | | [pam.tf](./pam.tf) | None | google_privileged_access_manager_entitlement | | [quotas.tf](./quotas.tf) | None | google_cloud_quotas_quota_preference | +| [scc-mute-configs.tf](./scc-mute-configs.tf) | Project-level SCC mute configurations. | google_scc_v2_project_mute_config | | [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 | @@ -2205,7 +2258,7 @@ module "project" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L237) | Project name and id suffix. | string | ✓ | | +| [name](variables.tf#L238) | Project name and id suffix. | string | ✓ | | | [alerts](variables-observability.tf#L17) | Monitoring alerts. | map(object({…})) | | {} | | [asset_feeds](variables.tf#L18) | Cloud Asset Inventory feeds. | map(object({…})) | | {} | | [auto_create_network](variables.tf#L51) | Whether to create the default network for the project. | bool | | false | @@ -2219,16 +2272,16 @@ module "project" { | [default_service_account](variables.tf#L161) | Project default service account setting: can be one of `delete`, `deprivilege`, `disable`, or `keep`. | string | | "keep" | | [deletion_policy](variables.tf#L174) | Deletion policy setting for this project. | string | | "DELETE" | | [descriptive_name](variables.tf#L185) | Descriptive project name. Set when name differs from project id. | string | | null | -| [factories_config](variables.tf#L191) | Paths to data files and folders that enable factory functionality. | object({…}) | | {} | +| [factories_config](variables.tf#L191) | 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)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | -| [kms_autokeys](variables.tf#L206) | KMS Autokey key handles. | map(object({…})) | | {} | -| [labels](variables.tf#L224) | Resource labels. | map(string) | | {} | -| [lien_reason](variables.tf#L231) | If non-empty, creates a project lien with this description. | string | | null | +| [kms_autokeys](variables.tf#L207) | KMS Autokey key handles. | map(object({…})) | | {} | +| [labels](variables.tf#L225) | Resource labels. | map(string) | | {} | +| [lien_reason](variables.tf#L232) | 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) | | {} | @@ -2237,25 +2290,26 @@ module "project" { | [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#L242) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | +| [org_policies](variables.tf#L243) | Organization policies applied to this project keyed by policy name. | map(object({…})) | | {} | | [pam_entitlements](variables-pam.tf#L17) | Privileged Access Manager entitlements for this resource, keyed by entitlement ID. | map(object({…})) | | {} | -| [parent](variables.tf#L270) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | -| [prefix](variables.tf#L284) | Optional prefix used to generate project id and name. | string | | null | -| [project_reuse](variables.tf#L294) | Reuse existing project if not null. If name and number are not passed in, a data source is used. | object({…}) | | null | +| [parent](variables.tf#L271) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [prefix](variables.tf#L285) | Optional prefix used to generate project id and name. | string | | null | +| [project_reuse](variables.tf#L295) | 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({…})) | | {} | -| [scc_sha_custom_modules](variables-scc.tf#L17) | SCC custom modules keyed by module name. | map(object({…})) | | {} | -| [service_agents_config](variables.tf#L314) | Automatic service agent configuration options. | object({…}) | | {} | -| [service_config](variables.tf#L325) | Configure service API activation. | object({…}) | | {…} | -| [service_encryption_key_ids](variables.tf#L337) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | -| [services](variables.tf#L344) | Service APIs to enable. | list(string) | | [] | -| [shared_vpc_host_config](variables.tf#L350) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | -| [shared_vpc_service_config](variables.tf#L360) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | -| [skip_delete](variables.tf#L397) | Deprecated. Use deletion_policy. | bool | | null | +| [scc_mute_configs](variables-scc.tf#L17) | SCC mute configurations keyed by name. | map(object({…})) | | {} | +| [scc_sha_custom_modules](variables-scc.tf#L28) | SCC custom modules keyed by module name. | map(object({…})) | | {} | +| [service_agents_config](variables.tf#L315) | Automatic service agent configuration options. | object({…}) | | {} | +| [service_config](variables.tf#L326) | Configure service API activation. | object({…}) | | {…} | +| [service_encryption_key_ids](variables.tf#L338) | Service Agents to be granted encryption/decryption permissions over Cloud KMS encryption keys. Format {SERVICE_AGENT => [KEY_ID]}. | map(list(string)) | | {} | +| [services](variables.tf#L345) | Service APIs to enable. | list(string) | | [] | +| [shared_vpc_host_config](variables.tf#L351) | Configures this project as a Shared VPC host project (mutually exclusive with shared_vpc_service_project). | object({…}) | | null | +| [shared_vpc_service_config](variables.tf#L361) | Configures this project as a Shared VPC service project (mutually exclusive with shared_vpc_host_config). | object({…}) | | {…} | +| [skip_delete](variables.tf#L398) | Deprecated. Use deletion_policy. | bool | | null | | [tag_bindings](variables-tags.tf#L89) | Tag bindings for this project, in key => tag value id format. | map(string) | | null | | [tags](variables-tags.tf#L96) | 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#L161) | Fine-grained control on tag resource and IAM creation. | object({…}) | | {} | -| [universe](variables.tf#L409) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | -| [vpc_sc](variables.tf#L420) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | +| [universe](variables.tf#L410) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | object({…}) | | null | +| [vpc_sc](variables.tf#L421) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | object({…}) | | null | | [workload_identity_pools](variables-identity-providers.tf#L17) | Workload Identity Federation pools and providers. | map(object({…})) | | {} | ## Outputs diff --git a/modules/project/scc-mute-configs.tf b/modules/project/scc-mute-configs.tf new file mode 100644 index 000000000..5b3c9e7d9 --- /dev/null +++ b/modules/project/scc-mute-configs.tf @@ -0,0 +1,54 @@ +/** + * 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 SCC mute configurations. + +locals { + _scc_mute_configs_factory_path = pathexpand(coalesce(var.factories_config.scc_mute_configs, "-")) + _scc_mute_configs_factory_data_raw = merge([ + for f in try(fileset(local._scc_mute_configs_factory_path, "*.yaml"), []) : + yamldecode(file("${local._scc_mute_configs_factory_path}/${f}")) + ]...) + _scc_mute_configs_factory_data = { + for k, v in local._scc_mute_configs_factory_data_raw : + k => { + description = try(v.description, null) + filter = v.filter + type = try(v.type, "DYNAMIC") + } + } + _scc_mute_configs = merge( + local._scc_mute_configs_factory_data, + var.scc_mute_configs + ) + scc_mute_configs = { + for k, v in local._scc_mute_configs : + k => merge(v, { + name = k + parent = "projects/${local.project.project_id}" + }) + } +} + +resource "google_scc_v2_project_mute_config" "scc_mute_configs" { + for_each = local.scc_mute_configs + project = local.project.project_id + location = "global" + mute_config_id = each.key + description = each.value.description + filter = each.value.filter + type = each.value.type +} diff --git a/modules/project/schemas/scc-mute-config.schema.json b/modules/project/schemas/scc-mute-config.schema.json new file mode 100644 index 000000000..6a46cb581 --- /dev/null +++ b/modules/project/schemas/scc-mute-config.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SCC Mute Configurations", + "type": "object", + "patternProperties": { + "^[a-zA-Z]+$": { + "type": "object", + "required": [ + "filter" + ], + "properties": { + "description": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "DYNAMIC", + "STATIC" + ], + "default": "DYNAMIC" + } + } + } + } +} diff --git a/modules/project/schemas/scc-mute-config.schema.md b/modules/project/schemas/scc-mute-config.schema.md new file mode 100644 index 000000000..15db0d6ce --- /dev/null +++ b/modules/project/schemas/scc-mute-config.schema.md @@ -0,0 +1,11 @@ +# SCC Mute Configurations + + + +## Properties + +- **`^[a-zA-Z]+$`**: *object* + - **description**: *string* + - ⁺**filter**: *string* + - **type**: *string* + - enum: `DYNAMIC`, `STATIC` diff --git a/modules/project/variables-scc.tf b/modules/project/variables-scc.tf index 115d7967f..f0a76382b 100644 --- a/modules/project/variables-scc.tf +++ b/modules/project/variables-scc.tf @@ -14,6 +14,17 @@ * limitations under the License. */ +variable "scc_mute_configs" { + description = "SCC mute configurations keyed by name." + type = map(object({ + description = optional(string) + filter = string + type = optional(string, "DYNAMIC") + })) + default = {} + nullable = false +} + variable "scc_sha_custom_modules" { description = "SCC custom modules keyed by module name." type = map(object({ diff --git a/modules/project/variables.tf b/modules/project/variables.tf index 098ec8f68..22471b1ea 100644 --- a/modules/project/variables.tf +++ b/modules/project/variables.tf @@ -196,6 +196,7 @@ variable "factories_config" { org_policies = optional(string) pam_entitlements = optional(string) quotas = optional(string) + scc_mute_configs = optional(string) scc_sha_custom_modules = optional(string) tags = optional(string) }) diff --git a/tests/modules/folder/examples/scc-mute-configs.yaml b/tests/modules/folder/examples/scc-mute-configs.yaml new file mode 100644 index 000000000..6470e3753 --- /dev/null +++ b/tests/modules/folder/examples/scc-mute-configs.yaml @@ -0,0 +1,34 @@ +# 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_v2_folder_mute_config.scc_mute_configs["muteHighSeverity"]: + description: Mute high severity findings + filter: severity="HIGH" + location: global + mute_config_id: muteHighSeverity + type: DYNAMIC + +counts: + google_folder: 1 + google_scc_v2_folder_mute_config: 1 + modules: 1 + resources: 2 diff --git a/tests/modules/organization/examples/scc-mute-configs.yaml b/tests/modules/organization/examples/scc-mute-configs.yaml new file mode 100644 index 000000000..bbfd4adc2 --- /dev/null +++ b/tests/modules/organization/examples/scc-mute-configs.yaml @@ -0,0 +1,27 @@ +# 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_v2_organization_mute_config.scc_mute_configs["muteHighSeverity"]: + description: Mute high severity findings + filter: severity="HIGH" + location: global + mute_config_id: muteHighSeverity + organization: '1122334455' + type: DYNAMIC + +counts: + google_scc_v2_organization_mute_config: 1 + modules: 1 + resources: 1 diff --git a/tests/modules/project/examples/scc-mute-configs.yaml b/tests/modules/project/examples/scc-mute-configs.yaml new file mode 100644 index 000000000..c8bfcee13 --- /dev/null +++ b/tests/modules/project/examples/scc-mute-configs.yaml @@ -0,0 +1,44 @@ +# 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_v2_project_mute_config.scc_mute_configs["muteHighSeverity"]: + description: Mute high severity findings + filter: severity="HIGH" + location: global + mute_config_id: muteHighSeverity + project: test-project + timeouts: null + type: DYNAMIC + +counts: + google_project: 1 + google_scc_v2_project_mute_config: 1 + modules: 1 + resources: 2