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
+