diff --git a/modules/cis-log-alerts/main.tf b/modules/cis-log-alerts/main.tf index 340ef8834..17432b37a 100644 --- a/modules/cis-log-alerts/main.tf +++ b/modules/cis-log-alerts/main.tf @@ -39,11 +39,11 @@ resource "google_monitoring_alert_policy" "alert_policy" { project = var.project # notification_channels = var.notification_channels - notification_channels = [google_monitoring_notification_channel.email.name] + notification_channels = concat([google_monitoring_notification_channel.email.name], var.notification_channels) display_name = each.key combiner = var.combiner conditions { - display_name = "each.key" + display_name = each.key condition_threshold { filter = each.value duration = var.duration diff --git a/modules/cis-log-alerts/variables.tf b/modules/cis-log-alerts/variables.tf index 01a310127..fadc34de0 100644 --- a/modules/cis-log-alerts/variables.tf +++ b/modules/cis-log-alerts/variables.tf @@ -39,6 +39,12 @@ variable "duration" { type = string } +variable "notification_channels" { + description = "Additional notification channels for alert policies." + type = list(string) + default = [] +} + variable "per_series_aligner" { description = "Per series aligner." type = string diff --git a/modules/cis-log-metrics/main.tf b/modules/cis-log-metrics/main.tf index 1b9dd31a8..fae82b01b 100644 --- a/modules/cis-log-metrics/main.tf +++ b/modules/cis-log-metrics/main.tf @@ -16,11 +16,11 @@ locals { log_filter = { project-owner-log = < { + "iam-bpa:${principal}//${role}" => { member = principal role = role condition = null } } - ]... + ]...), + var.iam_bindings_additive ) } diff --git a/modules/organization-se/variables.tf b/modules/organization-se/variables.tf index 0ef099644..a3c7ecc55 100644 --- a/modules/organization-se/variables.tf +++ b/modules/organization-se/variables.tf @@ -97,7 +97,7 @@ variable "organization_id" { description = "Organization id in organizations/nnnnnn format." type = string validation { - condition = can(regex("^organizations/[0-9]+", var.organization_id)) + condition = can(regex("^organizations/[0-9]+$", var.organization_id)) error_message = "The organization_id must in the form organizations/nnn." } } diff --git a/modules/spanner-instance-se/iam.tf b/modules/spanner-instance-se/iam.tf index 582d2bb14..af3e44cf1 100644 --- a/modules/spanner-instance-se/iam.tf +++ b/modules/spanner-instance-se/iam.tf @@ -45,6 +45,14 @@ resource "google_spanner_instance_iam_binding" "bindings" { instance = local.spanner_instance.id role = each.value.role members = each.value.members + dynamic "condition" { + for_each = try(each.value.condition, null) == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } } resource "google_spanner_instance_iam_member" "bindings" { diff --git a/modules/spanner-instance-se/variables.tf b/modules/spanner-instance-se/variables.tf index d2a80a784..97f00c935 100644 --- a/modules/spanner-instance-se/variables.tf +++ b/modules/spanner-instance-se/variables.tf @@ -81,6 +81,11 @@ variable "iam_bindings" { type = map(object({ members = list(string) role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) })) nullable = false default = {} diff --git a/tests/tools/test_blueprint_and_module_validations.py b/tests/tools/test_blueprint_and_module_validations.py index d7da966ae..9c25e2512 100644 --- a/tests/tools/test_blueprint_and_module_validations.py +++ b/tests/tools/test_blueprint_and_module_validations.py @@ -55,5 +55,33 @@ def test_modules_enforce_30_char_id_preconditions(self): ) + + def test_custom_stellar_engine_modules_validations(self): + """Verify hardening and logic fixes across custom Stellar Engine modules.""" + alerts_main = (REPO_ROOT / "modules/cis-log-alerts/main.tf").read_text(encoding="utf-8") + self.assertIn("concat([google_monitoring_notification_channel.email.name], var.notification_channels)", alerts_main) + self.assertIn("display_name = each.key", alerts_main) + self.assertNotIn('display_name = "each.key"', alerts_main) + + alerts_vars = (REPO_ROOT / "modules/cis-log-alerts/variables.tf").read_text(encoding="utf-8") + self.assertIn('variable "notification_channels"', alerts_vars) + + metrics_main = (REPO_ROOT / "modules/cis-log-metrics/main.tf").read_text(encoding="utf-8") + self.assertIn("AND ((ProjectOwnership OR projectOwnerInvitee)", metrics_main) + + ids_main = (REPO_ROOT / "modules/intrusion-detection-system/main.tf").read_text(encoding="utf-8") + self.assertIn("count = var.create_service_networking_connection ? 1 : 0", ids_main) + + org_se_iam = (REPO_ROOT / "modules/organization-se/iam.tf").read_text(encoding="utf-8") + self.assertIn('"iam-bpa:${principal}//${role}"', org_se_iam) + org_se_vars = (REPO_ROOT / "modules/organization-se/variables.tf").read_text(encoding="utf-8") + self.assertIn('can(regex("^organizations/[0-9]+$", var.organization_id))', org_se_vars) + + spanner_se_iam = (REPO_ROOT / "modules/spanner-instance-se/iam.tf").read_text(encoding="utf-8") + self.assertIn('dynamic "condition"', spanner_se_iam) + spanner_se_vars = (REPO_ROOT / "modules/spanner-instance-se/variables.tf").read_text(encoding="utf-8") + self.assertIn("condition = optional(object({", spanner_se_vars) + + if __name__ == "__main__": unittest.main()