diff --git a/fast/stages/0-org-setup/datasets/classic/observability/iac-0/impersonation.yaml b/fast/stages/0-org-setup/datasets/classic/observability/iac-0/impersonation.yaml
new file mode 100644
index 000000000..632be9ef2
--- /dev/null
+++ b/fast/stages/0-org-setup/datasets/classic/observability/iac-0/impersonation.yaml
@@ -0,0 +1,63 @@
+# Copyright 2026 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.
+
+# yaml-language-server: $schema=../../../../schemas/observability.schema.json
+
+notification_channels:
+ email-security:
+ type: email
+ display_name: Security Team Email
+ labels:
+ email_address: $email_addresses:gcp-organization-admins
+ enabled: true
+
+logging_metrics:
+ sa-impersonation:
+ filter: |
+ protoPayload.serviceName="iamcredentials.googleapis.com"
+ (protoPayload.methodName="GenerateAccessToken" OR protoPayload.methodName="GenerateIdToken")
+ label_extractors:
+ email_id: EXTRACT(resource.labels.email_id)
+ metric_descriptor:
+ metric_kind: DELTA
+ value_type: INT64
+ unit: "1"
+ display_name: Service Account Impersonation
+ labels:
+ - key: email_id
+ value_type: STRING
+
+alerts:
+ sa-impersonation-alert:
+ display_name: Service Account Impersonation Alert
+ combiner: OR
+ conditions:
+ - display_name: Impersonation Detected
+ condition_threshold:
+ filter: |
+ metric.type="logging.googleapis.com/user/sa-impersonation" AND
+ resource.type="global"
+ comparison: COMPARISON_GT
+ threshold_value: 0
+ duration: 60s
+ trigger:
+ count: 1
+ aggregations:
+ alignment_period: 60s
+ per_series_aligner: ALIGN_COUNT
+ cross_series_reducer: REDUCE_SUM
+ group_by_fields: ["metric.label.email_id"]
+ notification_channels:
+ - email-security
+ enabled: true
diff --git a/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.yaml b/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.yaml
index 0b234f28f..cb1ba257e 100644
--- a/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.yaml
+++ b/fast/stages/0-org-setup/datasets/classic/projects/core/iac-0.yaml
@@ -1,4 +1,4 @@
-# Copyright 2025 Google LLC
+# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -83,6 +83,8 @@ org_policies:
- https://token.actions.githubusercontent.com
- https://gitlab.com
- https://app.terraform.io
+factories_config:
+ observability: datasets/classic/observability/iac-0
data_access_logs:
storage.googleapis.com:
DATA_READ: {}
@@ -90,6 +92,10 @@ data_access_logs:
sts.googleapis.com:
DATA_READ: {}
DATA_WRITE: {}
+ # required to capture service account impersonation events
+ iam.googleapis.com:
+ DATA_READ: {}
+ DATA_WRITE: {}
buckets:
# Terraform state bucket for this stage
iac-org-state:
diff --git a/fast/stages/0-org-setup/schemas/observability.schema.json b/fast/stages/0-org-setup/schemas/observability.schema.json
new file mode 100644
index 000000000..cf3eb2f0a
--- /dev/null
+++ b/fast/stages/0-org-setup/schemas/observability.schema.json
@@ -0,0 +1,514 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Observability Schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "alerts": {
+ "$ref": "#/$defs/alerts"
+ },
+ "logging_metrics": {
+ "$ref": "#/$defs/logging_metrics"
+ },
+ "notification_channels": {
+ "$ref": "#/$defs/notification_channels"
+ }
+ },
+ "$defs": {
+ "alerts": {
+ "title": "Alerts",
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-zA-Z0-9-]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "combiner": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "notification_channels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "severity": {
+ "type": "string"
+ },
+ "user_labels": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "alert_strategy": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "auto_close": {
+ "type": "string"
+ },
+ "notification_prompts": {
+ "type": "string"
+ },
+ "notification_rate_limit": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "period": {
+ "type": "string"
+ }
+ }
+ },
+ "notification_channel_strategy": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "notification_channel_names": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "renotify_interval": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "conditions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/condition"
+ }
+ },
+ "documentation": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "mime_type": {
+ "type": "string"
+ },
+ "subject": {
+ "type": "string"
+ },
+ "links": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "display_name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "combiner"
+ ]
+ }
+ }
+ },
+ "logging_metrics": {
+ "title": "Logging Metrics",
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-zA-Z0-9-]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "filter": {
+ "type": "string"
+ },
+ "bucket_name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "disabled": {
+ "type": "boolean"
+ },
+ "label_extractors": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "value_extractor": {
+ "type": "string"
+ },
+ "bucket_options": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "explicit_buckets": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "bounds": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "exponential_buckets": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "num_finite_buckets": {
+ "type": "number"
+ },
+ "growth_factor": {
+ "type": "number"
+ },
+ "scale": {
+ "type": "number"
+ }
+ }
+ },
+ "linear_buckets": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "num_finite_buckets": {
+ "type": "number"
+ },
+ "width": {
+ "type": "number"
+ },
+ "offset": {
+ "type": "number"
+ }
+ }
+ }
+ }
+ },
+ "metric_descriptor": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "metric_kind": {
+ "type": "string"
+ },
+ "value_type": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "unit": {
+ "type": "string"
+ },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "value_type": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key"
+ ]
+ }
+ }
+ },
+ "required": [
+ "metric_kind",
+ "value_type"
+ ]
+ }
+ },
+ "required": [
+ "filter"
+ ]
+ }
+ }
+ },
+ "notification_channels": {
+ "title": "Notification Channels",
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^[a-zA-Z0-9-]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "labels": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "user_labels": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "sensitive_labels": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "auth_token": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "service_key": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "required": [
+ "type"
+ ]
+ }
+ }
+ },
+ "condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "display_name": {
+ "type": "string"
+ },
+ "condition_absent": {
+ "$ref": "#/$defs/absent_condition"
+ },
+ "condition_matched_log": {
+ "$ref": "#/$defs/matched_log_condition"
+ },
+ "condition_monitoring_query_language": {
+ "$ref": "#/$defs/monitoring_query_condition"
+ },
+ "condition_prometheus_query_language": {
+ "$ref": "#/$defs/prometheus_query_condition"
+ },
+ "condition_threshold": {
+ "$ref": "#/$defs/threshold_condition"
+ }
+ },
+ "required": [
+ "display_name"
+ ]
+ },
+ "absent_condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "duration": {
+ "type": "string"
+ },
+ "filter": {
+ "type": "string"
+ },
+ "aggregations": {
+ "$ref": "#/$defs/aggregations"
+ },
+ "trigger": {
+ "$ref": "#/$defs/trigger"
+ }
+ },
+ "required": [
+ "duration"
+ ]
+ },
+ "matched_log_condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "filter": {
+ "type": "string"
+ },
+ "label_extractors": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "filter"
+ ]
+ },
+ "monitoring_query_condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "duration": {
+ "type": "string"
+ },
+ "query": {
+ "type": "string"
+ },
+ "evaluation_missing_data": {
+ "type": "string"
+ },
+ "trigger": {
+ "$ref": "#/$defs/trigger"
+ }
+ },
+ "required": [
+ "duration",
+ "query"
+ ]
+ },
+ "prometheus_query_condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "query": {
+ "type": "string"
+ },
+ "alert_rule": {
+ "type": "string"
+ },
+ "disable_metric_validation": {
+ "type": "boolean"
+ },
+ "duration": {
+ "type": "string"
+ },
+ "evaluation_interval": {
+ "type": "string"
+ },
+ "labels": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "rule_group": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "query"
+ ]
+ },
+ "threshold_condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "comparison": {
+ "type": "string"
+ },
+ "duration": {
+ "type": "string"
+ },
+ "denominator_filter": {
+ "type": "string"
+ },
+ "evaluation_missing_data": {
+ "type": "string"
+ },
+ "filter": {
+ "type": "string"
+ },
+ "threshold_value": {
+ "type": "number"
+ },
+ "aggregations": {
+ "$ref": "#/$defs/aggregations"
+ },
+ "denominator_aggregations": {
+ "$ref": "#/$defs/aggregations"
+ },
+ "forecast_options": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "forecast_horizon": {
+ "type": "string"
+ }
+ }
+ },
+ "trigger": {
+ "$ref": "#/$defs/trigger"
+ }
+ },
+ "required": [
+ "comparison",
+ "duration"
+ ]
+ },
+ "aggregations": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "per_series_aligner": {
+ "type": "string"
+ },
+ "group_by_fields": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "cross_series_reducer": {
+ "type": "string"
+ },
+ "alignment_period": {
+ "type": "string"
+ }
+ }
+ },
+ "trigger": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "count": {
+ "type": "number"
+ },
+ "percent": {
+ "type": "number"
+ }
+ }
+ }
+ }
+}
diff --git a/fast/stages/0-org-setup/schemas/observability.schema.md b/fast/stages/0-org-setup/schemas/observability.schema.md
new file mode 100644
index 000000000..e3e411782
--- /dev/null
+++ b/fast/stages/0-org-setup/schemas/observability.schema.md
@@ -0,0 +1,166 @@
+# Observability Schema
+
+
+
+## Properties
+
+*additional properties: false*
+
+- **alerts**: *reference([alerts](#refs-alerts))*
+- **logging_metrics**: *reference([logging_metrics](#refs-logging_metrics))*
+- **notification_channels**: *reference([notification_channels](#refs-notification_channels))*
+
+## Definitions
+
+- **alerts**: *object*
+
*additional properties: false*
+ - **`^[a-zA-Z0-9-]+$`**: *object*
+
*additional properties: false*
+ - ⁺**combiner**: *string*
+ - **display_name**: *string*
+ - **enabled**: *boolean*
+ - **notification_channels**: *array*
+ - items: *string*
+ - **severity**: *string*
+ - **user_labels**: *object*
+
*additional properties: string*
+ - **alert_strategy**: *object*
+
*additional properties: false*
+ - **auto_close**: *string*
+ - **notification_prompts**: *string*
+ - **notification_rate_limit**: *object*
+
*additional properties: false*
+ - **period**: *string*
+ - **notification_channel_strategy**: *object*
+
*additional properties: false*
+ - **notification_channel_names**: *array*
+ - items: *string*
+ - **renotify_interval**: *string*
+ - **conditions**: *array*
+ - items: *reference([condition](#refs-condition))*
+ - **documentation**: *object*
+
*additional properties: false*
+ - **content**: *string*
+ - **mime_type**: *string*
+ - **subject**: *string*
+ - **links**: *array*
+ - items: *object*
+
*additional properties: false*
+ - **display_name**: *string*
+ - **url**: *string*
+- **logging_metrics**: *object*
+
*additional properties: false*
+ - **`^[a-zA-Z0-9-]+$`**: *object*
+
*additional properties: false*
+ - ⁺**filter**: *string*
+ - **bucket_name**: *string*
+ - **description**: *string*
+ - **disabled**: *boolean*
+ - **label_extractors**: *object*
+
*additional properties: string*
+ - **value_extractor**: *string*
+ - **bucket_options**: *object*
+
*additional properties: false*
+ - **explicit_buckets**: *object*
+
*additional properties: false*
+ - **bounds**: *array*
+ - items: *number*
+ - **exponential_buckets**: *object*
+
*additional properties: false*
+ - **num_finite_buckets**: *number*
+ - **growth_factor**: *number*
+ - **scale**: *number*
+ - **linear_buckets**: *object*
+
*additional properties: false*
+ - **num_finite_buckets**: *number*
+ - **width**: *number*
+ - **offset**: *number*
+ - **metric_descriptor**: *object*
+
*additional properties: false*
+ - ⁺**metric_kind**: *string*
+ - ⁺**value_type**: *string*
+ - **display_name**: *string*
+ - **unit**: *string*
+ - **labels**: *array*
+ - items: *object*
+
*additional properties: false*
+ - ⁺**key**: *string*
+ - **description**: *string*
+ - **value_type**: *string*
+- **notification_channels**: *object*
+
*additional properties: false*
+ - **`^[a-zA-Z0-9-]+$`**: *object*
+
*additional properties: false*
+ - ⁺**type**: *string*
+ - **description**: *string*
+ - **display_name**: *string*
+ - **enabled**: *boolean*
+ - **labels**: *object*
+
*additional properties: string*
+ - **user_labels**: *object*
+
*additional properties: string*
+ - **sensitive_labels**: *object*
+
*additional properties: false*
+ - **auth_token**: *string*
+ - **password**: *string*
+ - **service_key**: *string*
+- **condition**: *object*
+
*additional properties: false*
+ - ⁺**display_name**: *string*
+ - **condition_absent**: *reference([absent_condition](#refs-absent_condition))*
+ - **condition_matched_log**: *reference([matched_log_condition](#refs-matched_log_condition))*
+ - **condition_monitoring_query_language**: *reference([monitoring_query_condition](#refs-monitoring_query_condition))*
+ - **condition_prometheus_query_language**: *reference([prometheus_query_condition](#refs-prometheus_query_condition))*
+ - **condition_threshold**: *reference([threshold_condition](#refs-threshold_condition))*
+- **absent_condition**: *object*
+
*additional properties: false*
+ - ⁺**duration**: *string*
+ - **filter**: *string*
+ - **aggregations**: *reference([aggregations](#refs-aggregations))*
+ - **trigger**: *reference([trigger](#refs-trigger))*
+- **matched_log_condition**: *object*
+
*additional properties: false*
+ - ⁺**filter**: *string*
+ - **label_extractors**: *object*
+
*additional properties: string*
+- **monitoring_query_condition**: *object*
+
*additional properties: false*
+ - ⁺**duration**: *string*
+ - ⁺**query**: *string*
+ - **evaluation_missing_data**: *string*
+ - **trigger**: *reference([trigger](#refs-trigger))*
+- **prometheus_query_condition**: *object*
+
*additional properties: false*
+ - ⁺**query**: *string*
+ - **alert_rule**: *string*
+ - **disable_metric_validation**: *boolean*
+ - **duration**: *string*
+ - **evaluation_interval**: *string*
+ - **labels**: *object*
+
*additional properties: string*
+ - **rule_group**: *string*
+- **threshold_condition**: *object*
+
*additional properties: false*
+ - ⁺**comparison**: *string*
+ - ⁺**duration**: *string*
+ - **denominator_filter**: *string*
+ - **evaluation_missing_data**: *string*
+ - **filter**: *string*
+ - **threshold_value**: *number*
+ - **aggregations**: *reference([aggregations](#refs-aggregations))*
+ - **denominator_aggregations**: *reference([aggregations](#refs-aggregations))*
+ - **forecast_options**: *object*
+
*additional properties: false*
+ - **forecast_horizon**: *string*
+ - **trigger**: *reference([trigger](#refs-trigger))*
+- **aggregations**: *object*
+
*additional properties: false*
+ - **per_series_aligner**: *string*
+ - **group_by_fields**: *array*
+ - items: *string*
+ - **cross_series_reducer**: *string*
+ - **alignment_period**: *string*
+- **trigger**: *object*
+
*additional properties: false*
+ - **count**: *number*
+ - **percent**: *number*
diff --git a/modules/project/alerts.tf b/modules/project/alerts.tf
index ef2139c06..a28a67dc4 100644
--- a/modules/project/alerts.tf
+++ b/modules/project/alerts.tf
@@ -135,7 +135,7 @@ resource "google_monitoring_alert_policy" "alerts" {
# first try to get a channel created by this module
google_monitoring_notification_channel.channels[x].name,
# otherwise check the context
- var.context.notification_channels[x],
+ local.ctx.notification_channels[x],
# if nothing else, use the provided channel as is
x
)
diff --git a/modules/project/logging-metrics.tf b/modules/project/logging-metrics.tf
index 59dc7088e..42ba6f29d 100644
--- a/modules/project/logging-metrics.tf
+++ b/modules/project/logging-metrics.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -71,7 +71,7 @@ resource "google_logging_metric" "metrics" {
disabled = each.value.disabled
bucket_name = try(
# first try to check the context
- var.context.log_buckets[each.value.bucket_name],
+ local.ctx.log_buckets[each.value.bucket_name],
# if nothing else, use the provided channel as is
each.value.bucket_name
)
diff --git a/modules/project/notification-channels.tf b/modules/project/notification-channels.tf
index 45d1678c6..1827b07f9 100644
--- a/modules/project/notification-channels.tf
+++ b/modules/project/notification-channels.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,8 +45,16 @@ resource "google_monitoring_notification_channel" "channels" {
description = each.value.description
display_name = each.value.display_name
enabled = each.value.enabled
- labels = each.value.labels
- user_labels = each.value.user_labels
+ labels = each.value.labels == null ? null : {
+ for k, v in each.value.labels :
+ # allow interpolation of email addresses and pubsub topics
+ k => try(
+ local.ctx.email_addresses[v],
+ local.ctx.pubsub_topics[v],
+ v
+ )
+ }
+ user_labels = each.value.user_labels
dynamic "sensitive_labels" {
for_each = each.value.sensitive_labels[*]
content {
diff --git a/tests/fast/stages/s0_org_setup/simple.yaml b/tests/fast/stages/s0_org_setup/simple.yaml
index c9c855a3a..16395beb7 100644
--- a/tests/fast/stages/s0_org_setup/simple.yaml
+++ b/tests/fast/stages/s0_org_setup/simple.yaml
@@ -1,4 +1,4 @@
-# Copyright 2025 Google LLC
+# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -65,6 +66,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -97,6 +99,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -129,6 +132,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -161,6 +165,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -193,6 +198,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -211,6 +217,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -228,6 +235,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -245,6 +253,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -357,6 +366,7 @@ values:
content_disposition: null
content_encoding: null
content_language: null
+ contexts: []
customer_encryption: []
deletion_policy: null
detect_md5hash: null
@@ -1112,6 +1122,14 @@ values:
condition: []
project: ft0-prod-billing-exp-0
role: roles/viewer
+ module.factory.module.projects-iam["iac-0"].google_project_iam_audit_config.default["iam.googleapis.com"]:
+ audit_log_config:
+ - exempted_members: []
+ log_type: DATA_READ
+ - exempted_members: []
+ log_type: DATA_WRITE
+ project: ft0-prod-iac-core-0
+ service: iam.googleapis.com
module.factory.module.projects-iam["iac-0"].google_project_iam_audit_config.default["storage.googleapis.com"]:
audit_log_config:
- exempted_members: []
@@ -1241,6 +1259,82 @@ values:
module.factory.module.projects["iac-0"].data.google_storage_project_service_account.gcs_sa[0]:
project: ft0-prod-iac-core-0
user_project: null
+ module.factory.module.projects["iac-0"].google_logging_metric.metrics["sa-impersonation"]:
+ bucket_name: null
+ bucket_options: []
+ description: null
+ disabled: null
+ filter: 'protoPayload.serviceName="iamcredentials.googleapis.com"
+
+ (protoPayload.methodName="GenerateAccessToken" OR protoPayload.methodName="GenerateIdToken")
+
+ '
+ label_extractors:
+ email_id: EXTRACT(resource.labels.email_id)
+ metric_descriptor:
+ - display_name: Service Account Impersonation
+ labels:
+ - description: ''
+ key: email_id
+ value_type: STRING
+ metric_kind: DELTA
+ unit: '1'
+ value_type: INT64
+ name: sa-impersonation
+ project: ft0-prod-iac-core-0
+ timeouts: null
+ value_extractor: null
+ module.factory.module.projects["iac-0"].google_monitoring_alert_policy.alerts["sa-impersonation-alert"]:
+ alert_strategy: []
+ combiner: OR
+ conditions:
+ - condition_absent: []
+ condition_matched_log: []
+ condition_monitoring_query_language: []
+ condition_prometheus_query_language: []
+ condition_sql: []
+ condition_threshold:
+ - aggregations:
+ - alignment_period: 60s
+ cross_series_reducer: REDUCE_SUM
+ group_by_fields:
+ - metric.label.email_id
+ per_series_aligner: ALIGN_COUNT
+ comparison: COMPARISON_GT
+ denominator_aggregations: []
+ denominator_filter: null
+ duration: 60s
+ evaluation_missing_data: null
+ filter: 'metric.type="logging.googleapis.com/user/sa-impersonation" AND
+
+ resource.type="global"
+
+ '
+ forecast_options: []
+ threshold_value: 0
+ trigger:
+ - count: 1
+ percent: null
+ display_name: Impersonation Detected
+ display_name: Service Account Impersonation Alert
+ documentation: []
+ enabled: true
+ project: ft0-prod-iac-core-0
+ severity: null
+ timeouts: null
+ user_labels: null
+ module.factory.module.projects["iac-0"].google_monitoring_notification_channel.channels["email-security"]:
+ description: null
+ display_name: Security Team Email
+ enabled: true
+ force_delete: false
+ labels:
+ email_address: $email_addresses:gcp-organization-admins
+ project: ft0-prod-iac-core-0
+ sensitive_labels: []
+ timeouts: null
+ type: email
+ user_labels: null
module.factory.module.projects["iac-0"].google_org_policy_policy.default["iam.workloadIdentityPoolProviders"]:
dry_run_spec: []
name: projects/ft0-prod-iac-core-0/policies/iam.workloadIdentityPoolProviders
@@ -2862,6 +2956,7 @@ values:
stage: GA
title: Custom role tagViewer
module.organization[0].google_tags_tag_key.default["context"]:
+ allowed_values_regex: null
description: Organization-level contexts.
parent: organizations/1234567890
purpose: null
@@ -2869,6 +2964,7 @@ values:
short_name: context
timeouts: null
module.organization[0].google_tags_tag_key.default["environment"]:
+ allowed_values_regex: null
description: Organization-level environments.
parent: organizations/1234567890
purpose: null
@@ -2876,6 +2972,7 @@ values:
short_name: environment
timeouts: null
module.organization[0].google_tags_tag_key.default["org-policies"]:
+ allowed_values_regex: null
description: Organization policy condition tags.
parent: organizations/1234567890
purpose: null
@@ -2910,7 +3007,6 @@ values:
input: null
output: null
triggers_replace: null
-
counts:
google_bigquery_dataset: 1
google_bigquery_default_service_account: 2
@@ -2918,17 +3014,20 @@ counts:
google_essential_contacts_contact: 1
google_folder: 10
google_folder_iam_binding: 44
+ google_logging_metric: 1
google_logging_organization_settings: 1
google_logging_organization_sink: 3
google_logging_project_bucket_config: 3
google_logging_project_settings: 2
+ google_monitoring_alert_policy: 1
+ google_monitoring_notification_channel: 1
google_org_policy_custom_constraint: 1
google_org_policy_policy: 37
google_organization_iam_audit_config: 1
google_organization_iam_binding: 37
google_organization_iam_custom_role: 9
google_project: 3
- google_project_iam_audit_config: 2
+ google_project_iam_audit_config: 3
google_project_iam_binding: 17
google_project_iam_member: 15
google_project_service: 33
@@ -2948,5 +3047,5 @@ counts:
google_tags_tag_value_iam_binding: 4
local_file: 9
modules: 50
- resources: 324
+ resources: 328
terraform_data: 4
diff --git a/tests/modules/project/context.tfvars b/tests/modules/project/context.tfvars
index 476f74134..db7277832 100644
--- a/tests/modules/project/context.tfvars
+++ b/tests/modules/project/context.tfvars
@@ -31,6 +31,12 @@ context = {
tag_values = {
"test/one" = "tagValues/1234567890"
}
+ log_buckets = {
+ audit = "logging.googleapis.com/projects/my-project/locations/global/buckets/audit-bucket"
+ }
+ notification_channels = {
+ email = "projects/my-project/notificationChannels/12345"
+ }
vpc_sc_perimeters = {
default = "accessPolicies/888933661165/servicePerimeters/default"
}
@@ -38,6 +44,41 @@ context = {
test = "projects/test-prod-audit-logs-0/topics/audit-logs"
}
}
+alerts = {
+ test-alert = {
+ combiner = "OR"
+ display_name = "Test Alert"
+ conditions = [{
+ display_name = "test-condition"
+ condition_threshold = {
+ comparison = "COMPARISON_GT"
+ duration = "60s"
+ filter = "resource.type=\"gce_instance\" AND metric.type=\"compute.googleapis.com/instance/cpu/utilization\""
+ }
+ }]
+ notification_channels = ["$notification_channels:email"]
+ }
+}
+logging_metrics = {
+ test-metric = {
+ filter = "resource.type=\"gce_instance\""
+ bucket_name = "$log_buckets:audit"
+ }
+}
+notification_channels = {
+ new-email = {
+ type = "email"
+ labels = {
+ email_address = "$email_addresses:default"
+ }
+ }
+ new-pubsub = {
+ type = "pubsub"
+ labels = {
+ topic = "$pubsub_topics:test"
+ }
+ }
+}
asset_feeds = {
test = {
billing_project = "test-project"
diff --git a/tests/modules/project/context.yaml b/tests/modules/project/context.yaml
index dd888c331..b45ea1424 100644
--- a/tests/modules/project/context.yaml
+++ b/tests/modules/project/context.yaml
@@ -44,6 +44,17 @@ values:
condition: []
crypto_key_id: projects/kms-central-prj/locations/europe-west1/keyRings/my-keyring/cryptoKeys/ew1-compute
role: roles/cloudkms.cryptoKeyEncrypterDecrypter
+ google_logging_metric.metrics["test-metric"]:
+ bucket_name: logging.googleapis.com/projects/my-project/locations/global/buckets/audit-bucket
+ bucket_options: []
+ description: null
+ disabled: null
+ filter: resource.type="gce_instance"
+ label_extractors: null
+ name: test-metric
+ project: my-project
+ timeouts: null
+ value_extractor: null
google_logging_project_sink.sink["test-pubsub"]:
custom_writer_identity: null
description: test-pubsub (Terraform-managed).
@@ -54,6 +65,60 @@ values:
name: test-pubsub
project: my-project
unique_writer_identity: true
+ google_monitoring_alert_policy.alerts["test-alert"]:
+ alert_strategy: []
+ combiner: OR
+ conditions:
+ - condition_absent: []
+ condition_matched_log: []
+ condition_monitoring_query_language: []
+ condition_prometheus_query_language: []
+ condition_sql: []
+ condition_threshold:
+ - aggregations: []
+ comparison: COMPARISON_GT
+ denominator_aggregations: []
+ denominator_filter: null
+ duration: 60s
+ evaluation_missing_data: null
+ filter: resource.type="gce_instance" AND metric.type="compute.googleapis.com/instance/cpu/utilization"
+ forecast_options: []
+ threshold_value: null
+ trigger: []
+ display_name: test-condition
+ display_name: Test Alert
+ documentation: []
+ enabled: true
+ notification_channels:
+ - projects/my-project/notificationChannels/12345
+ project: my-project
+ severity: null
+ timeouts: null
+ user_labels: null
+ google_monitoring_notification_channel.channels["new-email"]:
+ description: null
+ display_name: null
+ enabled: true
+ force_delete: false
+ labels:
+ email_address: foo@example.com
+ project: my-project
+ sensitive_labels: []
+ timeouts: null
+ type: email
+ user_labels: null
+ google_monitoring_notification_channel.channels["new-pubsub"]:
+ description: null
+ display_name: null
+ enabled: true
+ force_delete: false
+ labels:
+ topic: projects/test-prod-audit-logs-0/topics/audit-logs
+ project: my-project
+ sensitive_labels: []
+ timeouts: null
+ type: pubsub
+ user_labels: null
google_privileged_access_manager_entitlement.default["net-admins"]:
additional_notification_targets: []
approval_workflow:
@@ -253,7 +318,10 @@ counts:
google_compute_shared_vpc_service_project: 1
google_essential_contacts_contact: 1
google_kms_crypto_key_iam_member: 1
+ google_logging_metric: 1
google_logging_project_sink: 1
+ google_monitoring_alert_policy: 1
+ google_monitoring_notification_channel: 2
google_privileged_access_manager_entitlement: 1
google_project: 1
google_project_iam_audit_config: 1
@@ -267,4 +335,4 @@ counts:
google_tags_tag_value_iam_binding: 2
google_tags_tag_value_iam_member: 1
modules: 0
- resources: 32
+ resources: 36
diff --git a/tools/duplicate-diff.py b/tools/duplicate-diff.py
index 479b75540..d014d9f21 100755
--- a/tools/duplicate-diff.py
+++ b/tools/duplicate-diff.py
@@ -60,6 +60,10 @@ duplicates = [
"fast/stages/2-security/schemas/folder.schema.json",
"modules/project-factory/schemas/folder.schema.json",
],
+ [
+ "fast/stages/0-org-setup/schemas/observability.schema.json",
+ "modules/project/schemas/observability.schema.json",
+ ],
[
"fast/stages/1-vpcsc/schemas/ingress-policy.schema.json",
"modules/vpc-sc/schemas/ingress-policy.schema.json",