diff --git a/.github/workflows/azo-tf-test.yml b/.github/workflows/azo-tf-test.yml
new file mode 100644
index 00000000..cc81cbf0
--- /dev/null
+++ b/.github/workflows/azo-tf-test.yml
@@ -0,0 +1,68 @@
+name: "Azure TF template tests"
+permissions:
+ contents: read
+on:
+ pull_request:
+ paths:
+ - 'azure-collection-terraform/**'
+
+jobs:
+
+ ValidateTF:
+ runs-on: ubuntu-latest
+ name: "Validatation (format & syntax)"
+ defaults:
+ run:
+ working-directory: ./azure-collection-terraform
+ steps:
+ - uses: actions/checkout@v4
+ name: Checkout source code
+
+ - uses: hashicorp/setup-terraform@v3
+ name: Setup Terraform
+ # The default configuration installs the latest version of Terraform CLI
+ # with:
+ # terraform_version: "1.1.7"
+
+ - name: Terraform fmt
+ id: fmt
+ run: terraform fmt -check -recursive -diff
+ continue-on-error: true
+
+ - name: Terraform Init
+ id: init
+ run: terraform init
+
+ - name: Terraform Validate
+ id: validate
+ run: terraform validate
+
+ TFSecurityChecksUsingCheckov:
+ name: "Security Checks (checkov)"
+ runs-on: "ubuntu-latest"
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+
+ - uses: bridgecrewio/checkov-action@master
+ with:
+ directory: 'azure-collection-terraform/'
+ quiet: true
+ framework: terraform
+ output_format: cli
+ output_bc_ids: false
+ download_external_modules: true
+ # skip_check:
+
+ TFSecurityChecksUsingTfsec:
+ name: "Security Checks (tfsec)"
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v3
+
+ - name: tfsec
+ uses: aquasecurity/tfsec-action@v1.0.0
+ with:
+ working_directory: 'azure-collection-terraform/'
diff --git a/azure-collection-terraform/README.md b/azure-collection-terraform/README.md
new file mode 100644
index 00000000..dc9fdb23
--- /dev/null
+++ b/azure-collection-terraform/README.md
@@ -0,0 +1,1754 @@
+# Azure Collection Terraform Module
+
+Terraform module for automated log and metrics collection from Azure resources to Sumo Logic.
+
+## 📑 Table of Contents
+
+> **Click on any section below to expand and view detailed contents**
+
+
+Overview & Architecture
+
+
+- [Overview](#overview)
+- [Architecture](#architecture)
+
+
+
+
+Terraform Resources
+
+
+**Azure Resources**
+- [azurerm_resource_group.rg](#azurerm_resource_grouprg)
+- [azurerm_eventhub_namespace.namespaces_by_location](#azurerm_eventhub_namespacenamespaces_by_location)
+- [azurerm_eventhub.eventhubs_by_type_and_location](#azurerm_eventhubeventhubs_by_type_and_location)
+- [azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy](#azurerm_eventhub_namespace_authorization_rulesumo_collection_policy)
+- [azurerm_monitor_diagnostic_setting.diagnostic_setting_logs](#azurerm_monitor_diagnostic_settingdiagnostic_setting_logs)
+- [Activity Logs Resources (Conditional)](#azurerm_eventhub_namespaceactivity_logs_namespace-conditional)
+
+**SumoLogic Resources**
+- [sumologic_collector.sumo_collector](#sumologic_collectorsumo_collector)
+- [sumologic_azure_event_hub_log_source](#sumologic_azure_event_hub_log_sourcesumo_azure_event_hub_log_source)
+- [sumologic_azure_metrics_source](#sumologic_azure_metrics_sourceterraform_azure_metrics_source)
+- [sumologic_app.apps](#sumologic_appapps)
+
+**Data Sources**
+- [azurerm_client_config.current](#azurerm_client_configcurrent)
+- [azurerm_resources.all_target_resources](#azurerm_resourcesall_target_resources)
+- [azurerm_monitor_diagnostic_categories.all_categories](#azurerm_monitor_diagnostic_categoriesall_categories)
+
+
+
+
+Requirements & Compatibility
+
+
+- [Configuration Files](#-configuration-files)
+- [Supported Versions](#supported-versions)
+- [Providers](#providers)
+- [Terraform Resource Reference](#terraform-resource-reference-provider-resources)
+
+
+
+
+Configuration
+
+
+- [Prerequisites](#prerequisites)
+- [Azure RBAC Requirements](#azure-rbac-requirements)
+ - Required Azure Roles
+ - Verify Your Permissions
+ - Authentication
+- [Configuration Variables](#configuration-variables)
+ - [Target Resource Types Configuration](#target-resource-types-configuration)
+ - [Log Source Filters Configuration](#log-source-filters-configuration)
+ - [Metrics Source Filters Configuration](#metrics-source-filters-configuration)
+ - [Activity Log Filters Configuration](#activity-log-filters-configuration)
+- [Provider Deletion Safety](#provider-deletion-safety)
+- [Important Notes](#important-notes)
+ - Event Hub SKU Configuration & Auto-Upgrade
+ - Activity Logs - Subscription-Level Warning
+
+
+
+
+How to Run This Project
+
+
+- [Step 1: Clone and Navigate](#step-1-clone-and-navigate)
+- [Step 2: Create Configuration File](#step-2-create-configuration-file)
+ - Required Variables (Must Configure)
+ - Optional Variables (Configure If Needed)
+ - Default Variables (Customize or Keep Defaults)
+- [Step 3: Initialize Terraform](#step-3-initialize-terraform)
+- [Step 4: Review Plan](#step-4-review-plan)
+- [Step 5: Deploy](#step-5-deploy)
+- [Step 6: Verify](#step-6-verify)
+- [Step 7: Access Dashboards and Monitors](#step-7-access-dashboards-and-monitors)
+
+
+
+
+Outputs
+
+
+- [Terraform Outputs](#outputs)
+
+
+
+
+Testing
+
+
+- [Test Structure](#test-structure)
+- [Running Tests](#running-tests)
+ - Quick Unit Tests
+ - Full Integration Tests
+ - Specific Test Categories
+- [Test Configuration](#test-configuration)
+- [Test Features](#test-features)
+
+
+
+
+Troubleshooting
+
+
+- [Azure Event Hub Regional Support](#azure-event-hub-regional-support)
+ - Regional Support Tables by Geography
+ - Quick Summary
+- [Common Issues](#common-issues)
+- [Debugging](#debugging)
+
+
+
+
+Cleanup
+
+
+- [Destroying Resources](#cleanup)
+- [Safe Cleanup Steps](#-to-safely-remove-activity-logs-before-destroying)
+
+
+
+---
+
+## Overview
+
+This module creates a complete data pipeline to collect logs and metrics from Azure resources and send them to Sumo Logic for monitoring and analysis. It automatically:
+
+- Discovers Azure resources based on tags
+- Creates EventHub infrastructure per location
+- Configures diagnostic settings for log collection
+- Sets up Sumo Logic sources and collectors
+
+## Architecture
+
+This module implements a dual-pipeline architecture for comprehensive Azure monitoring.
+
+### 📊 Log Collection Data Flow
+
+The following sequence outlines how Azure resource logs are collected and sent to Sumo Logic:
+
+1. **Azure Resources** generate diagnostic logs (access logs, audit logs, error logs) → View in: [Azure Portal](https://portal.azure.com) → Resource Groups → Resources
+
+2. **→ Diagnostic Settings** automatically route logs from all categories of each resource to Event Hubs → View in: Azure Portal → Resource → Monitoring → Diagnostic settings
+
+3. **→ Event Hub Namespaces & Hubs** receive and buffer logs organized by region and resource type → View in: Azure Portal → Resource Groups → Event Hub Namespaces
+
+4. **→ Sumo Logic Log Sources** consume logs from Event Hubs in real-time and parse JSON format → View in: [Sumo Logic](https://service.sumologic.com) → Manage Data → Collection → Collectors
+
+5. **→ Sumo Logic Apps & Dashboards** provide pre-built visualizations, searches, and alerts → Access in: Sumo Logic → App Catalog → Installed Apps
+
+### 📈 Metrics Collection Data Flow
+
+The following sequence outlines how Azure resource metrics are collected and sent to Sumo Logic:
+
+1. **Azure Resources** emit performance metrics (CPU, memory, throughput) automatically collected by Azure Monitor
+
+2. **→ Azure Monitor Metrics API** provides centralized access to all resource metrics via REST API → [Documentation](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/data-platform-metrics)
+
+3. **→ Sumo Logic Metrics Sources** poll Azure Monitor API at regular intervals for metrics data → View in: Sumo Logic → Manage Data → Collection → Metrics Sources
+
+4. **→ Sumo Logic Apps & Dashboards** visualize time-series data and provide metric-based alerts → Access in: Sumo Logic → App Catalog → Installed Apps
+
+### 🔑 Key Components Summary
+
+| Component | Purpose | Where to View |
+|-----------|---------|---------------|
+| **Resource Discovery** | Queries Azure for resources matching specified tags and types | Azure Portal → Resources |
+| **EventHub Infrastructure** | Regional log streaming backbone for efficient routing | Azure Portal → Event Hub Namespaces |
+| **Diagnostic Settings** | Configures each resource to stream logs to EventHubs | Azure Portal → Resource → Monitoring → Diagnostic settings |
+| **Sumo Logic Collectors** | Hosted collectors that receive logs and metrics | Sumo Logic → Manage Data → Collection |
+| **Sumo Logic Sources** | Log sources (EventHub) and Metrics sources (API polling) | Sumo Logic → Collectors → Sources |
+| **Sumo Logic Apps** | Pre-built dashboards, searches, and monitors | Sumo Logic → App Catalog |
+
+## Terraform Resources
+
+### Azure Resources
+Following are the list of Azure Terraform Resources managed by this module.
+#### azurerm_resource_group.rg
+Creates a resource group to contain all EventHub infrastructure.
+- **Purpose**: Logical container for EventHub namespaces and hubs
+- **Configuration**: Set via `resource_group_name` and `location` variables
+- **Lifecycle**: Managed by Terraform; deletion removes all contained resources
+
+#### azurerm_eventhub_namespace.namespaces_by_location
+Creates EventHub namespaces, one per unique Azure region where target resources exist.
+- **Purpose**: Regional EventHub service for high-performance log streaming
+- **Naming**: `{eventhub_namespace}-{location}` (e.g., `sumo-logs-eastus`)
+- **SKU**: Configurable via `eventhub_namespace_sku` (default: Standard)
+- **Scaling**: Capacity units set via `eventhub_namespace_capacity`
+
+#### azurerm_eventhub.eventhubs_by_type_and_location
+Creates individual EventHubs for each resource type and location combination.
+- **Purpose**: Separate data streams per resource type for organized log collection
+- **Naming**: `insights-logs-{resource_type}` per namespace
+- **Configuration**: Partition count and message retention configurable
+- **Cardinality**: One EventHub per (`resource_type` × `location`) combination
+
+#### azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy
+Creates authorization rules for Sumo Logic to access EventHub namespaces.
+- **Purpose**: Secure authentication for Sumo Logic log sources
+- **Permissions**: Listen only (read access)
+- **Scope**: One per namespace (location-based)
+- **Usage**: Connection strings used by Sumo Logic sources
+
+#### azurerm_monitor_diagnostic_setting.diagnostic_setting_logs
+Attaches diagnostic settings to each target Azure resource for log streaming.
+- **Purpose**: Routes resource logs to appropriate EventHub
+- **Configuration**: Automatically maps resources to EventHubs based on type and location
+- **Log Categories**: Streams all available diagnostic log categories
+- **Metrics**: Can optionally include metrics data
+
+#### azurerm_eventhub_namespace.activity_logs_namespace (Conditional)
+Creates a dedicated namespace for Azure subscription-level activity logs.
+- **Purpose**: Separate infrastructure for subscription audit logs
+- **Condition**: Created when `enable_activity_logs = true`
+- **Isolation**: Keeps subscription logs separate from resource logs
+- **Location**: Uses primary subscription region
+
+#### azurerm_eventhub.eventhub_for_activity_logs (Conditional)
+Creates the EventHub within activity logs namespace.
+- **Purpose**: Receives subscription-level activity and audit logs
+- **Naming**: `insights-activity-logs`
+- **Condition**: Created when `enable_activity_logs = true`
+- **Special Use**: Captures subscription-wide operations and changes
+
+#### azurerm_eventhub_namespace_authorization_rule.activity_logs_policy (Conditional)
+Creates authorization rule for Sumo Logic to access activity logs.
+- **Purpose**: Secure authentication for activity log collection
+- **Permissions**: Listen only
+- **Condition**: Created when `enable_activity_logs = true`
+
+#### azurerm_monitor_diagnostic_setting.activity_logs_to_event_hub (Conditional)
+Configures subscription-level diagnostic settings to stream activity logs.
+- **Purpose**: Routes subscription audit logs to dedicated EventHub
+- **Scope**: **Subscription-level** (not resource-level)
+- **Condition**: Created when `enable_activity_logs = true`
+- **Categories**: Administrative, Security, ServiceHealth, Alert, Recommendation, Policy, Autoscale
+- **⚠️ Important**: Azure allows only **ONE** activity log diagnostic setting per subscription
+ - Creating this setting will **replace** any existing activity log diagnostic setting
+ - Deleting this (via `terraform destroy`) will **remove** activity logs for the **entire subscription**
+ - Coordinate with your team to avoid conflicts on shared subscriptions
+
+### SumoLogic Resources
+
+Following are the list of Sumologic Terraform Resources managed by this module.
+
+#### sumologic_collector.sumo_collector
+Creates a hosted collector in Sumo Logic for receiving logs and metrics.
+- **Purpose**: Central collection point for all Azure data
+- **Type**: Hosted (cloud-based, no agent required)
+- **Configuration**: Named via `collector_name` variable
+- **Capacity**: Single collector handles all sources
+
+#### sumologic_azure_event_hub_log_source.sumo_azure_event_hub_log_source
+Creates log sources that consume from EventHubs.
+- **Purpose**. Reads logs from EventHubs and ingests into Sumo Logic.
+- **Cardinality**. One source per EventHub (filtered by `log_namespace`).
+- **Authentication**. Uses connection strings from authorization rules.
+- **Processing**. Parses Azure diagnostic log JSON format.
+- **Filtering**. Only created for resources with `log_namespace` defined.
+
+#### sumologic_azure_metrics_source.terraform_azure_metrics_source
+Creates metrics sources for Azure metrics collection via API.
+- **Purpose**: Collects metrics directly from Azure Monitor API
+- **Cardinality**: One source per unique metric_namespace group
+- **Authentication**: Uses configured Azure credentials
+- **Polling**: Periodic collection (configurable interval)
+- **Filtering**: Only created for resources with `metric_namespace` defined
+
+#### sumologic_azure_event_hub_log_source.sumo_activity_log_source (Conditional)
+Creates a dedicated source for subscription activity logs.
+- **Purpose**: Ingests subscription-level audit and activity logs
+- **Condition**: Created when `enable_activity_logs = true`
+- **Isolation**: Separate from resource log sources
+- **Usage**: Security auditing, compliance, change tracking
+
+#### sumologic_app.apps
+Installs pre-built Sumo Logic apps for visualization and analysis.
+- **Purpose**: Provides dashboards, searches, and alerts for Azure services
+- **Configuration**: Specified via `installation_apps_list` variable
+- **Examples**: Azure SQL, Azure Storage, Azure Kubernetes Service apps
+- **Requirement**: Each app requires specific log/metric sources to be configured
+
+### Data Sources
+
+Following are the list of Data Sources managed by this module.
+
+#### azurerm_client_config.current
+Retrieves current Azure authentication context.
+- **Usage**: Gets subscription ID and tenant ID for resource tagging
+- **Read-Only**: Does not create resources
+
+#### azurerm_resources.all_target_resources
+Queries Azure for resources matching specified tags.
+- **Purpose**: Discovers target resources for log collection
+- **Filter**: Uses `resource_group_name_filter` and `resource_tags` variables
+- **Output**: List of resource IDs, types, and locations
+- **Dynamic**: Results determine EventHub and diagnostic setting creation
+
+#### azurerm_monitor_diagnostic_categories.all_categories
+Retrieves available diagnostic categories for each discovered resource.
+- **Purpose**: Determines what logs and metrics each resource can export
+- **Dynamic**: Queries per discovered resource
+- **Usage**: Ensures diagnostic settings enable all available log categories
+
+## Requirements & compatibility
+
+This section documents the module's runtime requirements and compatibility expectations. It highlights which Terraform and provider versions the module is tested against, and which runtime tools are required for integration tests. Use supported versions to avoid unexpected provider schema or feature mismatches.
+
+### 📁 Configuration Files
+
+- `terraform.tfvars.example` - Template with all variables and detailed documentation
+- `terraform.tfvars` - Your actual configuration (customize and keep secure)
+- `test/` - Comprehensive test suite with 69 test scenarios
+
+### Supported versions
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.5.7 |
+| [azurerm](#requirement\_azurerm) | >= 4.19.0 |
+| [random](#requirement\_random) | >= 3.1.0 |
+| [sumologic](#requirement\_sumologic) | >= 3.0.2 |
+| [time](#requirement\_time) | >= 0.7.1 |
+
+### Providers
+
+| Name | Version |
+|------|---------|
+| [azurerm](#provider\_azurerm) | 4.43.0 |
+| [sumologic](#provider\_sumologic) | 3.1.4 |
+
+
+### Terraform resource reference (provider resources)
+
+| Name | Type |
+|------|------|
+| [azurerm_eventhub.eventhub_for_activity_logs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/eventhub) | resource |
+| [azurerm_eventhub.eventhubs_by_type_and_location](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/eventhub) | resource |
+| [azurerm_eventhub_namespace.activity_logs_namespace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/eventhub_namespace) | resource |
+| [azurerm_eventhub_namespace.namespaces_by_location](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/eventhub_namespace) | resource |
+| [azurerm_eventhub_namespace_authorization_rule.activity_logs_policy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/eventhub_namespace_authorization_rule) | resource |
+| [azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/eventhub_namespace_authorization_rule) | resource |
+| [azurerm_monitor_diagnostic_setting.activity_logs_to_event_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_diagnostic_setting) | resource |
+| [azurerm_monitor_diagnostic_setting.diagnostic_setting_logs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_diagnostic_setting) | resource |
+| [azurerm_resource_group.rg](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource |
+| [sumologic_azure_event_hub_log_source.sumo_activity_log_source](https://registry.terraform.io/providers/SumoLogic/sumologic/latest/docs/resources/azure_event_hub_log_source) | resource |
+| [sumologic_azure_event_hub_log_source.sumo_azure_event_hub_log_source](https://registry.terraform.io/providers/SumoLogic/sumologic/latest/docs/resources/azure_event_hub_log_source) | resource |
+| [sumologic_azure_metrics_source.terraform_azure_metrics_source](https://registry.terraform.io/providers/SumoLogic/sumologic/latest/docs/resources/azure_metrics_source) | resource |
+| [sumologic_collector.sumo_collector](https://registry.terraform.io/providers/SumoLogic/sumologic/latest/docs/resources/collector) | resource |
+| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source |
+| [azurerm_monitor_diagnostic_categories.all_categories](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/monitor_diagnostic_categories) | data source |
+| [azurerm_resources.all_target_resources](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resources) | data source |
+
+## Configuration
+
+### Prerequisites
+
+Before running this module, ensure you have:
+
+1. **Azure**:
+ - Active Azure subscription
+ - Azure CLI configured and authenticated (`az login`)
+ - Required Azure RBAC roles (see [Azure RBAC Requirements](#azure-rbac-requirements) below)
+
+2. **Sumo Logic**:
+ - Active Sumo Logic account
+ - Access ID and Access Key with permissions to create collectors and sources
+ - Admin role recommended for app installation
+
+3. **Terraform**:
+ - Terraform >= 1.5.7 installed
+ - Azure provider >= 4.19.0
+ - Sumo Logic provider >= 3.0.2
+
+### Azure RBAC Requirements
+
+This module uses **Azure CLI authentication** for Terraform operations. The Azure CLI user needs appropriate roles assigned.
+
+#### 🔵 **Required Azure Roles**
+
+**Option A: Single Role (Recommended for Simplicity)** ⭐
+```bash
+# Assign Contributor role at subscription level
+az role assignment create \
+ --assignee \
+ --role "Contributor" \
+ --scope "/subscriptions/"
+```
+
+**Option B: Principle of Least Privilege (More Secure)**
+```bash
+# 1. Reader - For resource discovery
+az role assignment create \
+ --assignee \
+ --role "Reader" \
+ --scope "/subscriptions/"
+
+# 2. Contributor - For EventHub infrastructure (specific resource group)
+az role assignment create \
+ --assignee \
+ --role "Contributor" \
+ --scope "/subscriptions//resourceGroups/"
+
+# 3. Monitoring Contributor - For diagnostic settings and metrics access
+az role assignment create \
+ --assignee \
+ --role "Monitoring Contributor" \
+ --scope "/subscriptions/"
+```
+
+**Required Permissions:**
+| Operation | Required Permission | Role |
+|-----------|-------------------|------|
+| Query resources by type/tags | `*/read` | Reader |
+| Read diagnostic categories | `Microsoft.Insights/DiagnosticSettings/Read` | Reader |
+| Create resource groups | `Microsoft.Resources/subscriptions/resourcegroups/write` | Contributor |
+| Create EventHub namespaces | `Microsoft.EventHub/namespaces/*` | Contributor |
+| Create EventHubs | `Microsoft.EventHub/namespaces/eventhubs/*` | Contributor |
+| Create authorization rules | `Microsoft.EventHub/namespaces/authorizationRules/*` | Contributor |
+| Create diagnostic settings | `Microsoft.Insights/DiagnosticSettings/Write` | Monitoring Contributor |
+| Activity logs (subscription) | `Microsoft.Insights/DiagnosticSettings/Write` | Monitoring Contributor |
+| Read Azure Monitor metrics | `Microsoft.Insights/Metrics/Read` | Monitoring Contributor |
+
+#### ✅ **Verify Your Permissions**
+
+Check your current role assignments:
+```bash
+# For current CLI user
+az role assignment list \
+ --assignee $(az account show --query user.name -o tsv) \
+ --output table
+```
+
+#### 📋 **Authentication**
+
+This module uses **Azure CLI Authentication**:
+- Setup: Run `az login` before executing Terraform
+- Terraform will use your Azure CLI session for all operations
+- Best for: Local development and manual deployments
+
+### Configuration Variables
+
+The following table describes all available configuration variables. For a complete example, see `terraform.tfvars.example`.
+
+| Name | Description | Type | Sample | Required |
+|------|-------------|------|---------|:--------:|
+| **Azure Authentication** |||||
+| `azure_subscription_id` | Azure Subscription ID where resources will be created. If not provided, uses current Azure CLI context. | `string` | `"your-azure-subscription-id"` | Yes |
+| `azure_client_id` | Service Principal Application (client) ID. If not provided, uses Azure CLI context. | `string` | `"your-azure-client-id"` | Yes |
+| `azure_client_secret` | Service Principal client secret value. If not provided, uses Azure CLI context. | `string` (sensitive) | `"your-azure-client-secret"` | Yes |
+| `azure_tenant_id` | Azure Active Directory Tenant ID. If not provided, uses current Azure CLI context. | `string` | `"your-azure-tenant-id"` | Yes |
+| **Azure Infrastructure** |||||
+| `resource_group_name` | Name for the new resource group where all Event Hub namespaces will be created. These Event Hubs are used to configure resource logs and activity logs collection sources in Sumo Logic. | `string` | `"sumologic-azure-collection-rg"` | **Yes** |
+| `eventhub_namespace_name` | Name for the Event Hub namespace (must be globally unique across Azure, 6-50 characters, starts with letter). | `string` | `"sumologic-azure-collection-EventHub"` | Yes |
+| `eventhub_namespace_sku` | Event Hub SKU tier (global default). Options: `"Basic"`, `"Standard"`, `"Premium"`, or `"Dedicated"`. Can be overridden per region using `region_specific_eventhub_skus`. Module automatically upgrades to Premium SKU when >10 Event Hubs exist in a region with Basic/Standard SKU. | `string` | `"Standard"` | **Yes** |
+| `default_throughput_units` | Default throughput units (processing capacity) for Event Hub namespaces. Valid values: `1`, `2`, `4`, `8`, or `16`. Applies to Standard and Premium SKUs. Can be overridden per region using `region_specific_eventhub_skus`. | `number` | `2` | **Yes** |
+| `region_specific_eventhub_skus` | Optional map to override SKU and throughput units for specific Azure regions. Each entry specifies `sku` and `throughput_units`. Region names are case-insensitive with spaces ignored. Use empty `{}` if no regional overrides needed.
**Example:**
```hcl
region_specific_eventhub_skus = {
"eastus" = {
sku = "Premium"
throughput_units = 4
}
"westindia" = {
sku = "Basic"
throughput_units = 1
}
}
```
**Auto-upgrade behavior:** When >10 Event Hubs exist in a region with Basic/Standard SKU, the module automatically upgrades to Premium SKU (unless explicitly overridden in this map). | `map(object({ sku = string, throughput_units = number }))` | `{}` | No |
+| `location` | Azure region for resources (must match subscription location). Example: `East US`, `West US 2`, `North Europe`. | `string` | `"East US"` | **Yes** |
+| `policy_name` | Name for the Event Hub authorization policy (1-64 characters, alphanumeric and hyphens only). | `string` | `"SumoLogicAzureCollectionPolicy"` | **Yes** |
+| **Activity Logs** |||||
+| `enable_activity_logs` | Enable/disable subscription-level activity log collection. **Warning**: Affects entire subscription, not just this Terraform workspace. | `bool` | `false` | **Yes** |
+| `activity_log_export_name` | Name for the activity log diagnostic setting (1-128 characters, alphanumeric with underscores, periods, hyphens). | `string` | `"SumoLogicAzureActivityLogExport"` | **Yes** |
+| `activity_log_export_category` | Source category for activity logs in Sumo Logic. | `string` | `"azure/activity-logs"` | **Yes** |
+| `activity_log_filters` | List of filters to apply to activity log sources in Sumo Logic. Each filter requires `filter_type` ("Include", "Exclude", or "Mask"), `name` (descriptive), `regexp` (RE2 pattern), and optional `mask` (replacement string, cannot contain colons). Filters are evaluated in order; first match wins. Use empty `[]` for no filtering. See [Activity Log Filters Configuration](#activity-log-filters-configuration) for details and examples. | `list(object)` | `[]` | No |
+| **Sumo Logic Configuration** |||||
+| `sumologic_access_id` | Sumo Logic Access ID from your account preferences. Used for API authentication. | `string` | `"your-sumologic-access-id"` | **Yes** |
+| `sumologic_access_key` | Sumo Logic Access Key from your account preferences. Used for API authentication. | `string` (sensitive) | `"your-sumologic-access-key"` | **Yes** |
+| `sumologic_environment` | Sumo Logic deployment region. Options: `us1`, `us2`, `eu`, `au`, `ca`, `de`, `jp`, `in`, `kr`, `fed`. | `string` | `"us1"` | **Yes** |
+| `sumologic_environment_base_url` | Base URL for custom Sumo Logic environments (e.g., `https://api.ch.sumologic.com/api/` for Switzerland). If provided, this overrides `sumologic_environment`. Use `null` for standard deployments. | `string` | `null` | **Yes** |
+| `scan_interval` | Polling interval (in milliseconds) for the Azure Metrics source. Controls how frequently metrics are collected from Azure Monitor API. Minimum value: `1000` (1 second). Default: `300000` (5 minutes). | `number` | `300000` | **Yes** |
+| `sumo_collector_name` | Name for the Sumo Logic hosted collector (alphanumeric, hyphens, underscores, max 128 characters). | `string` | `"sumologic-azure-collection"` | **Yes** |
+| `installation_apps_list` | List of Sumo Logic apps to install automatically. Each app requires `uuid`, `name`, `version`, and optionally `parameters` (map of key-value pairs for app configuration). Use empty `[]` to skip app installation. | `list(object)` | Refer to [terraform.tfvars.example](/azure-collection-terraform/terraform.tfvars.example#L285) | No |
+| **Resource Targeting** |||||
+| `target_resource_types` | List of Azure resource types to monitor. Supports multiple optional fields for filtering and customization. See [Target Resource Types Configuration](#target-resource-types-configuration) for detailed field descriptions, examples, and usage patterns. | `list(object)` | Refer to [terraform.tfvars.example](/azure-collection-terraform/terraform.tfvars.example#L87) | **Yes** |
+| `nested_namespace_configs` | Map of parent resource types to their child resource types for nested resources (e.g., Storage Accounts → blobServices/fileServices). Use empty `{}` if not monitoring nested resources. | `map(list(string))` | Refer to [terraform.tfvars.example](/azure-collection-terraform/terraform.tfvars.example#L258) | **Yes** |
+
+To obtain these values follow [these steps](https://www.sumologic.com/help/docs/send-data/hosted-collectors/microsoft-source/azure-metrics-source/#vendor-configuration)
+
+#### Target Resource Types Configuration
+
+The `target_resource_types` variable is a list of objects where each object defines which Azure resources to monitor and how to collect their data. This table details all available fields:
+
+| Field Name | Type | Required | Description | Examples |
+|------------|------|:--------:|-------------|----------|
+| **Basic Configuration** ||||||
+| `log_namespace` | `string` | No* | Azure resource type for log collection via Event Hub. Defines which resource type's logs to stream to Sumo Logic. | `"Microsoft.Storage/storageAccounts"`
`"Microsoft.KeyVault/vaults"` |
+| `metric_namespace` | `string` | No* | Azure resource type for metrics collection via Azure Monitor API. Defines which resource type's metrics to poll from Azure. | `"Microsoft.Storage/storageAccounts"`
`"Microsoft.Compute/virtualMachines"` |
+| **Log Category Filtering** ||||||
+| `log_categories` | `list(string)` | No | Specific diagnostic log categories to collect. If omitted or empty `[]`, collects ALL available categories. Invalid categories cause validation errors during `terraform plan`. | `["AuditEvent"]`
`["PostgreSQLLogs", "PostgreSQLFlexSessions"]`
`[]` (all categories) |
+| **Resource Discovery Filtering** ||||||
+| `required_resource_tags` | `map(string)` | No | **[Azure-side filtering]** Filter which resources are discovered and monitored by matching Azure tags (AND logic - all tags must match). Case-sensitive. Applies to both logs and metrics. If omitted or empty `{}`, monitors all resources of this type. | `{"environment" = "production"}`
`{"team" = "security", "critical" = "true"}` |
+| `name_filter` | `string` | No | **[Azure-side filtering]** Filter which resources are discovered by regex pattern on resource name (case-insensitive). **Only applies to log collection (`log_namespace`), not metrics.** For metrics filtering, use `required_resource_tags`. If omitted or empty `""`, monitors all resources of this type. | `"^prod-.*"` (starts with prod-)
`".*-test$"` (ends with -test)
`"^(prod\|staging)-.*"` (multiple prefixes) |
+| **Content Filtering (Sumo Logic Source Level)** ||||||
+| `log_source_filters` | `list(object)` | No | **[Sumo Logic-side filtering]** Filter, exclude, or mask log content at the Sumo Logic Event Hub source after logs are collected from Azure. Each filter is an object with specific fields (see sub-table below). Evaluated in order; first match wins. Empty `[]` = no filtering. | See [Log Source Filters Configuration](#log-source-filters-configuration) |
+| `metrics_source_filters` | `list(object)` | No | **[Sumo Logic-side filtering]** Filter, exclude, or mask metrics content at the Sumo Logic Azure metrics source after metrics are collected from Azure Monitor API. Each filter is an object with specific fields (see sub-table below). Evaluated in order; first match wins. Empty `[]` = no filtering. **Note:** Unlike log filters, metrics filters do not support the `regions` field. | See [Metrics Source Filters Configuration](#metrics-source-filters-configuration) |
+
+**\*At least one of `log_namespace` or `metric_namespace` must be specified per entry.**
+
+**Filtering Stages:**
+- **Stage 1 - Azure Discovery** (`required_resource_tags`, `name_filter`): Controls **which Azure resources** are monitored (happens in Azure)
+- **Stage 2 - Sumo Logic Ingestion** (`log_source_filters`, `metrics_source_filters`): Controls **which log/metrics messages** are ingested from discovered resources (happens in Sumo Logic)
+
+**Example with all fields:**
+```hcl
+target_resource_types = [
+ {
+ # Basic Configuration
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+
+ # Log Category Filtering
+ log_categories = ["AuditEvent", "AzurePolicyEvaluationDetails"]
+
+ # Resource Discovery Filtering
+ required_resource_tags = {
+ "environment" = "production"
+ "team" = "security"
+ }
+ name_filter = "^prod-.*" # Only production key vaults
+
+ # Content Filtering (Sumo Logic)
+ log_source_filters = [
+ {
+ filter_type = "Include"
+ name = "Include only successful operations"
+ regexp = ".*\"ResultType\":\"Success\".*"
+ regions = ["East US"] # Region-specific filter
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask sensitive data"
+ regexp = "(\\\"secretValue\\\"\\s*:\\s*\\\"[^\\\"]+\\\")"
+ mask = "***REDACTED***"
+ # No regions = applies globally
+ }
+ ]
+
+ # Metrics Content Filtering (Sumo Logic)
+ metrics_source_filters = [
+ {
+ filter_type = "Include"
+ name = "Include specific metrics"
+ regexp = ".*\"metricName\":\"(CPU|Memory).*"
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask sensitive metric values"
+ regexp = "(\\d{16})"
+ mask = "MASKED_VALUE"
+ }
+ ]
+ }
+]
+```
+
+**Common Patterns:**
+
+
+Logs Only (No Metrics)
+
+```hcl
+{
+ log_namespace = "Microsoft.Web/sites"
+ # Omit metric_namespace - no metrics collected
+}
+```
+
+
+
+Metrics Only (No Logs)
+
+```hcl
+{
+ metric_namespace = "Microsoft.Compute/virtualMachines"
+ # Omit log_namespace - no logs collected
+}
+```
+
+
+
+Both Logs and Metrics
+
+```hcl
+{
+ log_namespace = "Microsoft.Storage/storageAccounts"
+ metric_namespace = "Microsoft.Storage/storageAccounts"
+}
+```
+
+
+
+Specific Log Categories
+
+```hcl
+{
+ log_namespace = "Microsoft.DBforPostgreSQL/flexibleServers"
+ log_categories = ["PostgreSQLLogs", "PostgreSQLFlexSessions"]
+}
+```
+
+
+
+Tag-Based Filtering
+
+```hcl
+{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ required_resource_tags = {
+ "environment" = "production"
+ "compliance" = "pci-dss"
+ }
+}
+```
+
+
+
+Name-Based Filtering (Logs Only)
+
+```hcl
+{
+ log_namespace = "Microsoft.Network/applicationGateways"
+ name_filter = "^prod-.*" # Only resources starting with "prod-"
+}
+```
+
+
+#### Log Source Filters Configuration
+
+The `log_source_filters` field allows content-level filtering at the Sumo Logic Event Hub log source.
+
+| Field Name | Type | Required | Description |
+|------------|------|:--------:|-------------|
+| `filter_type` | `string` | **Yes** | Filter type: `"Include"` (only matching logs), `"Exclude"` (reject matching logs), or `"Mask"` (redact content) |
+| `name` | `string` | **Yes** | Descriptive name shown in Sumo Logic UI |
+| `regexp` | `string` | **Yes** | RE2 regex pattern matching the **entire log message** (JSON string). Use `.*` prefix/suffix for partial matches |
+| `mask` | `string` | No | Replacement string for Mask filters. **Cannot contain colons (:)** |
+| `regions` | `list(string)` | No | Azure regions where filter applies (case-insensitive). Empty `[]` or omitted = all regions globally |
+
+**Key Points:**
+- Filters process in order; first match wins
+- Regex must match entire JSON message structure
+- Mask strings cannot contain colons
+- Region names normalized ("East US" = "eastus")
+
+**Example:**
+```hcl
+log_source_filters = [
+ {
+ filter_type = "Include"
+ name = "Include successful operations"
+ regexp = ".*\"ResultType\":\"Success\".*"
+ regions = ["East US"] # Region-specific
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask credit cards"
+ regexp = "(\\d{16})"
+ mask = "XXXX-XXXX-XXXX-XXXX"
+ # Omit regions = applies globally
+ }
+]
+```
+
+#### Metrics Source Filters Configuration
+
+The `metrics_source_filters` field allows content-level filtering at the Sumo Logic Azure metrics source.
+
+| Field Name | Type | Required | Description |
+|------------|------|:--------:|-------------|
+| `filter_type` | `string` | **Yes** | Filter type: `"Include"` (only matching metrics), `"Exclude"` (reject matching metrics), or `"Mask"` (redact content) |
+| `name` | `string` | **Yes** | Descriptive name shown in Sumo Logic UI |
+| `regexp` | `string` | **Yes** | RE2 regex pattern matching the **entire metrics message** (JSON string). Use `.*` prefix/suffix for partial matches |
+| `mask` | `string` | No | Replacement string for Mask filters. **Cannot contain colons (:)** |
+
+**Key Points:**
+- Filters process in order; first match wins
+- **No `regions` field** (unlike log filters) - metrics filters apply globally
+- Regex must match entire JSON message structure
+- Mask strings cannot contain colons
+
+**Example:**
+```hcl
+metrics_source_filters = [
+ {
+ filter_type = "Include"
+ name = "Include specific resources"
+ regexp = ".*\"resourceId\":\"[^\"]*SBUS002[^\"]*\".*"
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask sensitive IDs"
+ regexp = "(\\d{16})"
+ mask = "MASKED_ID"
+ }
+]
+```
+
+#### Activity Log Filters Configuration
+
+The `activity_log_filters` variable applies filters to subscription-level activity log sources in Sumo Logic. These filters work identically to `log_source_filters`, but **do not support the `regions` field** (activity logs are subscription-wide, not region-specific).
+
+| Field Name | Type | Required | Description | Examples |
+|------------|------|:--------:|-------------|----------|
+| `filter_type` | `string` | **Yes** | Type of filter: `"Include"`, `"Exclude"`, or `"Mask"`. | `"Include"`
`"Mask"` |
+| `name` | `string` | **Yes** | Descriptive name for the filter. | `"Include Write operations only"` |
+| `regexp` | `string` | **Yes** | RE2 regex pattern (must match entire log message). | `".*\"operationName\".*write.*"` |
+| `mask` | `string` | No | Replacement string for `"Mask"` filters. **Cannot contain colons**. | `"***REDACTED***"` |
+
+**Example:**
+```hcl
+activity_log_filters = [
+ {
+ filter_type = "Include"
+ name = "Include Write operations only"
+ regexp = ".*\"operationName\"\\s*:\\s*\"[^\"]*write[^\"]*\".*"
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask sensitive fields"
+ regexp = "(\\\"password\\\".*)"
+ mask = "***REDACTED***"
+ }
+]
+```
+
+**Key Differences from Log Source Filters:**
+- ❌ No `regions` field (subscription-level, not regional)
+- ✅ Same filter types: Include, Exclude, Mask
+- ✅ Same mask validation (no colons)
+- ✅ Same evaluation order (first match wins)
+
+To obtain these values follow [these steps](https://www.sumologic.com/help/docs/send-data/hosted-collectors/microsoft-source/azure-metrics-source/#vendor-configuration)
+
+#### Provider deletion safety
+
+This module exposes a boolean variable named `prevent_deletion_if_contains_resources` which controls the azurerm provider feature `resource_group.prevent_deletion_if_contains_resources`.
+
+- Default: `true` (safe for production) — the provider will prevent deletion of a Resource Group if it still contains nested resources.
+- For automated integration tests you may set this to `false` so the tests can clean up Resource Groups that still contain nested Azure resources (test-only usage).
+
+Example (override via CLI):
+
+```
+terraform apply -var 'prevent_deletion_if_contains_resources=false'
+```
+
+Or in a tfvars file:
+
+```hcl
+prevent_deletion_if_contains_resources = false
+```
+
+
+#### Important Notes
+
+**Event Hub SKU Configuration & Auto-Upgrade:**
+
+This module provides flexible Event Hub SKU configuration with intelligent auto-upgrade capabilities:
+
+**SKU Selection Priority (per region):**
+1. **Region-Specific Override** → If configured in `region_specific_eventhub_skus`, uses that SKU/throughput
+2. **Auto-Upgrade to Premium** → If >10 Event Hubs exist in region with Basic/Standard SKU, automatically upgrades to Premium
+3. **Limited SKU Regions** → Regions with SKU restrictions (West India, Mexico Central) automatically use Standard SKU
+4. **Global Default** → Falls back to `eventhub_namespace_sku` and `default_throughput_units`
+
+**Auto-Upgrade Behavior:**
+- **Trigger**: When a region contains >10 Event Hubs and is using Basic or Standard SKU
+- **Action**: Automatically upgrades to Premium SKU for that region
+- **Reason**: Azure limits Basic/Standard SKUs to 10 Event Hubs per namespace; Premium supports up to 100
+- **Override**: Can be prevented by explicitly setting the region's SKU in `region_specific_eventhub_skus`
+
+**Example Configuration:**
+```hcl
+eventhub_namespace_sku = "Standard" # Global default
+default_throughput_units = 2 # Global default throughput
+
+region_specific_eventhub_skus = {
+ "eastus" = {
+ sku = "Premium" # Override: Use Premium in East US
+ throughput_units = 4
+ }
+ "westindia" = {
+ sku = "Basic" # Override: Use Basic in West India
+ throughput_units = 1
+ }
+}
+# Result:
+# - East US: Premium (4 TU) - explicit override
+# - West India: Basic (1 TU) - explicit override
+# - Other regions: Standard (2 TU) - global default
+# - Regions with >10 Event Hubs: Auto-upgraded to Premium (unless explicitly overridden)
+```
+
+**Regional SKU Limitations:**
+- **West India** and **Mexico Central**: Only support Basic and Standard SKUs (Premium not available)
+- The module automatically handles these limitations by defaulting to Standard SKU in these regions
+- You can still override to Basic SKU using `region_specific_eventhub_skus` if needed
+
+---
+
+**Activity Logs - Subscription-Level Warning:**
+
+Activity logs operate at the **Azure subscription level**, not at the resource level. This has important implications:
+
+- **Scope**. Activity logs capture ALL operations across the ENTIRE subscription (all resource groups, all resources).
+- **Single Configuration**. Azure only allows **ONE** diagnostic setting for activity logs per subscription.
+- **Shared Resource**. If multiple Terraform configurations or users enable activity logs on the same subscription, **only the last one applied will be active**.
+- **Deletion Impact**. Running `terraform destroy` on ANY Terraform configuration that manages activity logs will **delete the subscription-level diagnostic setting**, affecting **ALL** other configurations that depend on it.
+
+**Best Practices:**
+1. ✅ Enable activity logs in **only ONE** Terraform workspace per subscription
+2. ✅ Coordinate with your team to avoid conflicts
+3. ✅ Consider managing activity logs separately from resource-level monitoring
+4. ⚠️ Be cautious when running `terraform destroy` - it will remove activity log collection for the entire subscription
+
+**Alternative Approach:**
+If multiple teams need activity logs, consider:
+- Creating a dedicated Terraform configuration for subscription-level resources (activity logs, policies, etc.)
+- Using `terraform import` to manage existing activity log settings
+- Documenting ownership and avoiding duplicate configurations
+
+## How to Run This Project
+
+### Step 1: Clone and Navigate
+
+```bash
+cd azure-collection-terraform
+```
+
+### Step 2: Create Configuration File
+
+Copy the `terraform.tfvars.example` file to `terraform.tfvars` and configure the variables as described below:
+
+```bash
+cp terraform.tfvars.example terraform.tfvars
+```
+
+Edit `terraform.tfvars` and configure variables according to these groups:
+
+#### 🔴 **Required - Must Configure** (Set these values for your environment)
+
+```hcl
+# ============================================================================
+# Azure Authentication - Get these from Azure Portal
+# ============================================================================
+azure_subscription_id = "your-azure-subscription-id" # Your Azure subscription ID
+azure_client_id = "your-azure-client-id" # Service principal client ID
+azure_client_secret = "your-azure-client-secret" # Service principal secret
+azure_tenant_id = "your-azure-tenant-id" # Your Azure AD tenant ID
+
+# ============================================================================
+# Event Hub Configuration - Choose SKU and throughput based on your needs
+# ============================================================================
+eventhub_namespace_sku = "Standard" # Options: "Basic", "Standard", "Premium", or "Dedicated"
+default_throughput_units = 2 # Throughput units: 1, 2, 4, 8, or 16 (applies to Standard/Premium SKUs)
+
+# Note: Module automatically upgrades to Premium SKU when >10 Event Hubs exist in a region with Basic/Standard SKU
+# Note: See "Optional" section below for region-specific SKU overrides (region_specific_eventhub_skus)
+
+# ============================================================================
+# Azure Region - Where to deploy Event Hub infrastructure for Activity Logs
+# ============================================================================
+location = "East US" # Example: "East US", "West Europe", "Southeast Asia"
+
+# ============================================================================
+# Activity Logs - Enable/disable subscription-level activity log collection
+# ============================================================================
+enable_activity_logs = false # Set to true to collect Azure activity logs
+
+# ============================================================================
+# Target Resources - Define which Azure resources to monitor
+# ============================================================================
+target_resource_types = [
+ {
+ log_namespace = "Microsoft.Storage/storageAccounts"
+ metric_namespace = "Microsoft.Storage/storageAccounts"
+ # Optional: Filter by tags (AND logic - all tags must match)
+ # required_resource_tags = {
+ # "environment" = "production"
+ # "monitoring" = "enabled"
+ # }
+ }
+ # Add more resource types as needed
+]
+
+# ============================================================================
+# Sumo Logic Configuration - Get these from your Sumo Logic account
+# ============================================================================
+sumologic_access_id = "your-sumologic-access-id" # From Sumo Logic → Preferences → My Access Keys
+sumologic_access_key = "your-sumologic-access-key" # From Sumo Logic → Preferences → My Access Keys
+sumologic_environment = "us1" # Options: us1, us2, eu, au, ca, de, jp, in, kr, fed
+
+# ============================================================================
+# Sumo Logic Apps - Specify apps to install automatically
+# ============================================================================
+installation_apps_list = [
+ {
+ uuid = "azure-storage-app-uuid"
+ name = "Azure Storage"
+ version = "1.0.3"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ }
+ # Add more apps as needed
+]
+```
+
+#### 🟡 **Optional - Configure If Needed** (Leave empty or customize)
+
+```hcl
+# ============================================================================
+# Regional SKU Overrides - Override SKU/throughput for specific regions
+# ============================================================================
+# region_specific_eventhub_skus = {
+# "eastus" = {
+# sku = "Premium"
+# throughput_units = 4
+# }
+# "westindia" = {
+# sku = "Basic"
+# throughput_units = 1
+# }
+# }
+# Leave as {} if using global defaults for all regions
+
+# ============================================================================
+# Nested Resources - Configure child resources for monitoring (optional)
+# ============================================================================
+nested_namespace_configs = {
+ # Example: Monitor blob services within storage accounts
+ # "Microsoft.Storage/storageAccounts" = [
+ # "Microsoft.Storage/storageAccounts/blobServices",
+ # "Microsoft.Storage/storageAccounts/fileServices"
+ # ]
+}
+# Leave as {} if not monitoring nested resources
+```
+
+#### 🟢 **Optional - Customize or Keep Defaults** (Pre-configured with sensible defaults)
+
+These variables have default values and typically don't need changes:
+
+```hcl
+# ============================================================================
+# Resource Naming - Customize if you want different names
+# ============================================================================
+resource_group_name = "sumologic-azure-collection-rg"
+eventhub_namespace_name = "sumologic-azure-collection-EventHub"
+policy_name = "SumoLogicAzureCollectionPolicy"
+activity_log_export_name = "SumoLogicAzureActivityLogExport"
+activity_log_export_category = "azure/activity-logs"
+sumo_collector_name = "sumologic-azure-collection"
+
+# ============================================================================
+# Regional Support - Pre-configured with known limitations
+# ============================================================================
+eventhub_namespace_unsupported_locations = [ /* 19 unsupported regions */ ]
+eventhub_namespace_limited_sku_locations = ["West India", "Mexico Central"]
+
+# ============================================================================
+# Provider Safety - Keep default for production
+# ============================================================================
+prevent_deletion_if_contains_resources = true # Safety feature - don't change
+```
+
+**💡 Quick Start Example:**
+
+For a minimal configuration to get started quickly:
+
+```hcl
+# Azure (Required)
+azure_subscription_id = "your-azure-subscription-id"
+azure_client_id = "your-azure-client-id"
+azure_client_secret = "your-azure-client-secret"
+azure_tenant_id = "your-azure-tenant-id"
+
+# Event Hub (Required)
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+
+# Monitoring (Required)
+enable_activity_logs = false
+target_resource_types = [
+ {
+ log_namespace = "Microsoft.Storage/storageAccounts"
+ metric_namespace = "Microsoft.Storage/storageAccounts"
+ # Optional: Filter this resource type by tags
+ # required_resource_tags = {
+ # "environment" = "production"
+ # "team" = "platform"
+ # }
+ }
+]
+
+# Sumo Logic (Required)
+sumologic_access_id = "your-access-id"
+sumologic_access_key = "your-access-key"
+sumologic_environment = "us1"
+installation_apps_list = [] # Start with no apps, add later
+
+# Optional - Leave as default
+nested_namespace_configs = {}
+```
+
+### Step 3: Initialize Terraform
+
+```bash
+terraform init
+```
+
+This downloads required providers (Azure, Sumo Logic, Random, Time).
+
+### Step 4: Review Plan
+
+```bash
+terraform plan
+```
+
+Review the resources to be created:
+- Resource group for EventHub infrastructure
+- EventHub namespaces (one per region)
+- EventHubs (one per resource type and location)
+- Diagnostic settings for each target resource
+- Sumo Logic collector, sources, and apps
+
+### Step 5: Deploy
+
+```bash
+terraform apply
+```
+
+Type `yes` when prompted. Deployment typically takes 5-10 minutes.
+
+### Step 6: Verify
+
+1. **Azure Portal**:
+ - Check resource group contains EventHub namespaces
+ - Verify diagnostic settings on target resources
+ - Confirm EventHubs are receiving data
+
+2. **Sumo Logic**:
+ - Navigate to Manage Data → Collection
+ - Verify collector is online
+ - Check sources show "Receiving Data"
+ - View installed apps in App Catalog
+
+### Step 7: Access Dashboards and Monitors
+
+Access your installed Sumo Logic content:
+
+1. **Dashboards**:
+ - Navigate to **Sumo Logic** → **App Catalog**
+ - Find your installed apps (e.g., "Azure Application Gateway", "Azure Key Vault")
+ - Click on the app name to view pre-built dashboards
+
+2. **Monitors** (Alerts):
+ - Navigate to **Sumo Logic** → **Manage Data** → **Monitoring** → **Monitors**
+ - Installed apps include monitor templates that you can configure
+ - Click **"+ Add"** → **"From Monitor Template"** to create monitors from installed app templates
+ - Configure thresholds, notification channels, and alert conditions
+ - Example monitors: High error rate, Resource unavailability, Performance degradation
+
+3. **Content Location**:
+ - All app content (dashboards, searches, monitors) is installed in your **Personal** folder by default
+ - You can move content to **Admin Recommended** folder for team-wide access
+ - Path: **Personal** → **[App Name]** (e.g., Personal → Azure Application Gateway)
+
+**💡 Tip**: Monitor templates provide pre-configured alerting for common issues. Customize thresholds based on your environment.
+
+## Outputs
+
+After deployment, Terraform outputs:
+
+| Output | Description |
+|--------|-------------|
+| `collector_id` | Sumo Logic collector ID |
+| `eventhub_namespace_ids` | Map of EventHub namespace IDs by location |
+| `eventhub_ids` | List of created EventHub IDs |
+| `log_source_ids` | List of Sumo Logic log source IDs |
+| `metrics_source_ids` | List of Sumo Logic metrics source IDs |
+| `resource_group_id` | Azure resource group ID |
+| `diagnostic_setting_ids` | Map of diagnostic setting IDs by resource |
+
+## Testing
+
+This module includes comprehensive tests to validate both the Terraform configuration and the Azure/SumoLogic integration.
+
+### Test Structure
+
+- **`test/azure_test.go`**: Integration tests that validate Azure resource creation and configuration
+- **`test/sumologic_test.go`**: Integration tests that validate SumoLogic resource configuration
+- **`test/basic_test.go`**: Basic Terraform syntax validation tests
+- **`test/diagnostic_setting_naming_test.go`**: Unit tests for Azure diagnostic setting naming logic
+- **`test/resource_type_parsing_test.go`**: Unit tests for resource type parsing and environment variable handling
+
+### Running Tests
+
+#### Quick Unit Tests (Fast - ~2 seconds)
+```bash
+cd test
+go test -v -run "TestResourceTypesParsing|TestTestEnvironmentHelperMethods|TestDiagnosticSettingNaming|TestBasicSyntax" -timeout 30s
+```
+
+#### Full Integration Tests (Requires Azure credentials - ~10-30 minutes)
+```bash
+cd test
+go test -v -timeout 30m
+```
+
+#### Specific Test Categories
+```bash
+# Unit tests only
+go test -v -run "TestResourceTypesParsing|TestDiagnosticSettingNaming"
+
+# Basic validation tests
+go test -v -run "TestBasicSyntax|TestBasicTerraformVariableValidation"
+
+# Azure integration tests
+go test -v -run "TestAzure.*"
+
+# SumoLogic integration tests
+go test -v -run "TestSumoLogic.*"
+```
+
+### Test Configuration
+
+Tests use environment variables from `.env.test` file. Copy `.env.test.example` to `.env.test` and configure:
+
+#### Required Azure Configuration
+```bash
+AZURE_SUBSCRIPTION_ID=your-subscription-id
+AZURE_CLIENT_ID=your-client-id
+AZURE_CLIENT_SECRET=your-client-secret
+AZURE_TENANT_ID=your-tenant-id
+```
+
+#### Required SumoLogic Configuration
+```bash
+SUMOLOGIC_ENVIRONMENT=us1
+SUMOLOGIC_ACCESS_ID=your-access-id
+SUMOLOGIC_ACCESS_KEY=your-access-key
+```
+
+#### Target Resource Types (Unified Format)
+The `TARGET_RESOURCE_TYPES` variable supports multiple formats:
+
+```bash
+# JSON Array Format (Recommended)
+TARGET_RESOURCE_TYPES=["Microsoft.KeyVault/vaults","Microsoft.Storage/storageAccounts"]
+
+# Comma-separated Format
+TARGET_RESOURCE_TYPES=Microsoft.KeyVault/vaults,Microsoft.Storage/storageAccounts
+
+# Single Resource Type
+TARGET_RESOURCE_TYPES=["Microsoft.KeyVault/vaults"]
+```
+
+#### Test Control Flags
+```bash
+RUN_INTEGRATION_TESTS=true # Enable integration tests
+CLEANUP_RESOURCES=true # Auto-cleanup test resources
+```
+
+### Test Features
+
+- **✅ Unified Resource Type Parsing**: Tests validate flexible parsing of `TARGET_RESOURCE_TYPES`
+- **✅ Diagnostic Setting Naming**: Tests ensure Azure naming compliance with SumoLogic conventions
+- **✅ Backward Compatibility**: Tests verify helper methods maintain compatibility with legacy variable names
+- **✅ Environment Validation**: Tests validate all required environment variables and configurations
+- **✅ Azure Resource Discovery**: Tests validate dynamic discovery of Azure resources
+- **✅ SumoLogic Integration**: Tests validate proper SumoLogic collector and source configuration
+
+## Troubleshooting
+
+### "HTTP response was nil" Errors During Deployment
+
+#### Problem
+During large deployments (100+ resources), you may see:
+```
+Error: creating Monitor Diagnostics Setting "..." HTTP response was nil; connection may have been reset
+```
+
+#### Root Cause
+Azure API throttling or transient network issues during high-volume operations.
+
+#### Solutions
+
+**1. Use the Deployment Script (Recommended)**
+```bash
+./deploy.sh deploy
+```
+
+This handles everything automatically including retries and imports.
+
+**2. Manual Deployment with Reduced Parallelism**
+```bash
+terraform apply -parallelism=5
+```
+
+Lower parallelism (5-10) prevents overwhelming Azure's API.
+
+**3. If Errors Still Occur - Recovery Process**
+
+The resource **is created in Azure** but not in terraform state. To fix:
+
+**Step 1**: Verify the resource exists:
+```bash
+az monitor diagnostic-settings show \
+ --resource "" \
+ --name ""
+```
+
+**Step 2**: Import it into terraform:
+```bash
+terraform import '' '|'
+```
+
+**Step 3**: Retry the apply:
+```bash
+terraform apply
+```
+
+#### Prevention Tips
+- Use `./deploy.sh` script for all deployments
+- For very large deployments (500+ resources), deploy in batches
+- Monitor Azure service health before deployments
+- Consider increasing `parallelism` gradually: 5 → 10 → 20
+
+### Azure Event Hub Limitation
+Please visit [here](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-quotas) to find out more details about Azure Event Hub Limitations
+
+### Azure Event Hub Regional Support
+
+The table below shows Event Hub namespace availability across Azure regions by SKU tier. This module automatically handles region-specific SKU limitations.
+
+**Legend:**
+- ✅ = **Fully Supported** - Event Hub namespace available for this SKU tier
+- ❌ = **Not Supported** - Event Hub namespace not available
+- ⚠️ = **Limited Support** - Only Basic/Standard SKUs available (Premium not supported)
+
+---
+
+#### 🌍 Africa
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| South Africa North | ✅ | ✅ | ✅ | Fully supported |
+| South Africa West | ❌ | ❌ | ❌ | No Event Hub support |
+
+#### 🌏 Asia Pacific
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| East Asia | ✅ | ✅ | ✅ | Fully supported |
+| Southeast Asia | ✅ | ✅ | ✅ | Fully supported |
+| Australia Central | ✅ | ✅ | ✅ | Fully supported |
+| Australia Central 2 | ❌ | ❌ | ❌ | No Event Hub support |
+| Australia East | ✅ | ✅ | ✅ | Fully supported |
+| Australia Southeast | ✅ | ✅ | ✅ | Fully supported |
+| Indonesia Central | ✅ | ✅ | ✅ | Fully supported |
+| Malaysia West | ✅ | ✅ | ✅ | Fully supported |
+| New Zealand North | ✅ | ✅ | ✅ | Fully supported |
+
+#### 🇨🇳 China
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| China East | ❌ | ❌ | ❌ | No Event Hub support |
+| China East 2 | ❌ | ❌ | ❌ | No Event Hub support |
+| China East 3 | ❌ | ❌ | ❌ | No Event Hub support |
+| China North | ❌ | ❌ | ❌ | No Event Hub support |
+| China North 2 | ❌ | ❌ | ❌ | No Event Hub support |
+| China North 3 | ❌ | ❌ | ❌ | No Event Hub support |
+
+#### 🇪🇺 Europe
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| North Europe | ✅ | ✅ | ✅ | Fully supported |
+| West Europe | ✅ | ✅ | ✅ | Fully supported |
+| Austria East | ✅ | ✅ | ✅ | Fully supported |
+| France Central | ✅ | ✅ | ✅ | Fully supported |
+| France South | ❌ | ❌ | ❌ | No Event Hub support |
+| Germany North | ❌ | ❌ | ❌ | No Event Hub support |
+| Germany West Central | ✅ | ✅ | ✅ | Fully supported |
+| Italy North | ✅ | ✅ | ✅ | Fully supported |
+| Norway East | ✅ | ✅ | ✅ | Fully supported |
+| Norway West | ❌ | ❌ | ❌ | No Event Hub support |
+| Poland Central | ✅ | ✅ | ✅ | Fully supported |
+| Spain Central | ✅ | ✅ | ✅ | Fully supported |
+| Sweden Central | ✅ | ✅ | ✅ | Fully supported |
+| Sweden South | ❌ | ❌ | ❌ | No Event Hub support |
+| Switzerland North | ✅ | ✅ | ✅ | Fully supported |
+| Switzerland West | ❌ | ❌ | ❌ | No Event Hub support |
+| UK South | ✅ | ✅ | ✅ | Fully supported |
+| UK West | ✅ | ✅ | ✅ | Fully supported |
+
+#### 🇮🇳 India
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| Central India | ✅ | ✅ | ✅ | Fully supported |
+| South India | ✅ | ✅ | ✅ | Fully supported |
+| West India | ✅ | ✅ | ⚠️ | **Premium SKU not available** - Module auto-downgrades to Standard |
+
+#### 🇯🇵 Japan
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| Japan East | ✅ | ✅ | ✅ | Fully supported |
+| Japan West | ✅ | ✅ | ✅ | Fully supported |
+
+#### 🇰🇷 Korea
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| Korea Central | ✅ | ✅ | ✅ | Fully supported |
+| Korea South | ✅ | ✅ | ✅ | Fully supported |
+
+#### 🌎 Americas
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| Brazil South | ✅ | ✅ | ✅ | Fully supported |
+| Brazil Southeast | ❌ | ❌ | ❌ | No Event Hub support |
+| Canada Central | ✅ | ✅ | ✅ | Fully supported |
+| Canada East | ✅ | ✅ | ✅ | Fully supported |
+| Chile Central | ✅ | ✅ | ✅ | Fully supported |
+| Mexico Central | ✅ | ✅ | ⚠️ | **Premium SKU not available** - Module auto-downgrades to Standard |
+
+#### 🇺🇸 United States
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| Central US | ✅ | ✅ | ✅ | Fully supported |
+| East US | ✅ | ✅ | ✅ | Fully supported |
+| East US 2 | ✅ | ✅ | ✅ | Fully supported |
+| North Central US | ✅ | ✅ | ✅ | Fully supported |
+| South Central US | ✅ | ✅ | ✅ | Fully supported |
+| West Central US | ✅ | ✅ | ✅ | Fully supported |
+| West US | ✅ | ✅ | ✅ | Fully supported |
+| West US 2 | ✅ | ✅ | ✅ | Fully supported |
+| West US 3 | ✅ | ✅ | ✅ | Fully supported |
+
+#### 🏛️ US Government
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| USGov Arizona | ❌ | ❌ | ❌ | No Event Hub support |
+| USGov Texas | ❌ | ❌ | ❌ | No Event Hub support |
+| USGov Virginia | ❌ | ❌ | ❌ | No Event Hub support |
+
+#### 🌐 Middle East
+
+| Region Name | Basic | Standard | Premium | Notes |
+|-------------|:-----:|:--------:|:-------:|-------|
+| Israel Central | ✅ | ✅ | ✅ | Fully supported |
+| Qatar Central | ✅ | ✅ | ✅ | Fully supported |
+| Taiwan North | ❌ | ❌ | ❌ | No Event Hub support |
+| UAE Central | ❌ | ❌ | ❌ | No Event Hub support |
+| UAE North | ✅ | ✅ | ✅ | Fully supported |
+
+---
+
+### Common Issues
+
+| Issue | Solution |
+|-------|----------|
+| **Error creating diagnostic settings - resource already has a diagnostic setting** | Each Azure resource can have a limited number of diagnostic settings. Check existing settings in Azure Portal or use `terraform import` to manage existing configurations. |
+| **EventHub throughput exceeded** | Increase `default_throughput_units` (valid values: `1`, `2`, `4`, `8`, or `16`) or upgrade to a higher SKU tier. Alternatively, use `region_specific_eventhub_skus` to configure higher capacity for specific regions. The module automatically upgrades to Premium SKU when >10 Event Hubs exist in a region. |
+| **Sumo Logic sources show "Not Receiving Data"** | 1. Verify diagnostic settings are active in Azure Portal
2. Check EventHub is receiving messages (Monitor → Metrics → Incoming Messages)
3. Confirm authorization rules have Listen permission
4. Verify Sumo Logic connection string is correct
5. Check collector is online in Sumo Logic
6. **If using filters**: Check filter configuration isn't too restrictive (see [Filter Configuration Guide](#filter-configuration-guide)) |
+| **All logs dropped after adding filters** | Check that Include filters aren't too restrictive. With Include filters present, logs must match to be ingested. Test regex patterns with sample logs. See [Filter Configuration Guide](#filter-configuration-guide). |
+| **Filter mask validation error: "mask string should not contain any colons"** | Sumo Logic API rejects mask strings containing colons (`:`). Remove all colons from `mask` values. Example: Change `"password":"***REDACTED***"` to `***REDACTED***`. See [Filter Mask Validation](#filter-mask-validation). |
+| **Region-specific filter applies globally** | Check HCL syntax - ensure trailing comma after `regions` field in object lists: `regions = ["East US"],` (note comma). See [Filter Regional Behavior](#filter-regional-behavior). |
+| **Event Hub namespace creation failed in specific region** | Check the [regional support table](#azure-event-hub-regional-support) above. The region may not support Event Hub namespaces or may have SKU restrictions. The module automatically handles unsupported regions, but you can manually configure `eventhub_namespace_unsupported_locations` if needed. |
+| **Premium SKU forced to Standard in certain regions** | West India and Mexico Central only support Basic and Standard SKUs. The module automatically downgrades to Standard SKU in these regions. No action required unless you need Premium features (use a different region). |
+| **App installation fails with "app already installed" error** | The app is already installed in your Sumo Logic account. Either:
• Remove the app from `installation_apps_list` to skip installation
• Manually uninstall the existing app in Sumo Logic UI
• Use `terraform import` to manage the existing app installation |
+| **Authentication failures with Azure provider** | 1. Ensure you've run `az login` and are authenticated
2. Verify your Azure credentials have sufficient permissions (Contributor + Monitoring Contributor roles)
3. Check subscription ID matches your target subscription
4. Run `az account show` to verify current context |
+| **Variables validation errors** | 1. Check `eventhub_namespace_name` is 6-50 characters, starts with letter, globally unique
2. Verify `default_throughput_units` is one of: `1`, `2`, `4`, `8`, `16`
3. Ensure `eventhub_namespace_sku` is one of: `"Basic"`, `"Standard"`, `"Premium"`, `"Dedicated"`
4. Validate `location` matches a valid Azure region name
5. Check `resource_group_name` doesn't contain special characters or spaces
6. If using `region_specific_eventhub_skus`, verify each entry has valid `sku` and `throughput_units` values |
+| **Invalid log categories validation error** | Terraform fails during `terraform plan` if you specify invalid log categories. **To find valid categories:** Use Azure CLI: `az monitor diagnostic-settings categories list --resource --query "logs[].name"` or check [Azure documentation](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/resource-logs-categories). **Quick fix:** Omit the `log_categories` field to collect all available logs. Note: Category names are case-sensitive. |
+
+---
+
+### Resource Tag Filtering Guide
+
+Filter which Azure resources are monitored by adding `required_resource_tags` to each resource type in `target_resource_types`.
+
+**Key Behavior:**
+- **AND Logic**: ALL specified tags must match
+- **Case-Sensitive**: Exact match required for keys and values
+- **Optional**: Omit or use `{}` to monitor all resources of that type
+- **Per-Resource-Type**: Different filters for different resource types
+
+**Example:**
+```hcl
+target_resource_types = [
+ {
+ log_namespace = "Microsoft.KeyVault/vaults"
+ required_resource_tags = {"environment" = "production"} # Only production vaults
+ },
+ {
+ log_namespace = "Microsoft.Storage/storageAccounts"
+ required_resource_tags = {} # All storage accounts
+ }
+]
+```
+
+**Verify which resources match:**
+```bash
+# Check resources and their tags
+az resource list --resource-type "Microsoft.KeyVault/vaults" \
+ --query "[].{name:name, tags:tags}" --output table
+
+# Verify in Terraform plan
+terraform plan 2>&1 | grep -A 5 "azurerm_monitor_diagnostic_setting"
+```
+
+**Common issues:**
+- **No resources discovered**: Tags don't match (check case sensitivity)
+- **Missing resources**: Verify ALL tags present (AND logic)
+- **Whitespace**: `"team": "platform "` ≠ `"team": "platform"`
+
+---
+
+### Resource Name Filtering Guide
+
+Filter which Azure resources are monitored by name using regex patterns with the `name_filter` field in `target_resource_types`.
+
+**Key Behavior:**
+- **Regex Matching**: Supports full regex syntax for flexible patterns
+- **Case-Insensitive**: Pattern matching ignores case
+- **Optional**: Omit or use `""` to monitor all resources of that type
+- **Per-Resource-Type**: Different patterns for different resource types
+- **Not Applied to Nested Resources**: Child resources inherit parent filtering
+- **⚠️ Logs Only**: Name filtering only applies to log collection (`log_namespace`), not metrics (`metric_namespace`)
+
+**For metrics filtering, use `required_resource_tags` instead.**
+
+**Common Patterns:**
+```hcl
+target_resource_types = [
+ {
+ log_namespace = "Microsoft.Compute/virtualMachines"
+ name_filter = "^prod-" # Starts with "prod-"
+ },
+ {
+ log_namespace = "Microsoft.Storage/storageAccounts"
+ name_filter = ".*-test$" # Ends with "-test"
+ },
+ {
+ log_namespace = "Microsoft.KeyVault/vaults"
+ name_filter = ".*critical.*" # Contains "critical"
+ },
+ {
+ log_namespace = "Microsoft.Network/loadBalancers"
+ name_filter = "^(prod|staging)-.*" # Starts with "prod-" or "staging-"
+ }
+]
+```
+
+**Verify which resources match:**
+```bash
+# List all resources and test your pattern
+az resource list --resource-type "Microsoft.Compute/virtualMachines" \
+ --query "[].name" --output table
+
+# Verify in Terraform plan
+terraform plan 2>&1 | grep -A 5 "azurerm_monitor_diagnostic_setting"
+```
+
+**Common issues:**
+- **No resources matched**: Check your regex pattern syntax
+- **Too many resources**: Pattern too broad (e.g., `"prod"` matches "production", "nonprod")
+- **Use anchors**: `^` (start) and `$` (end) for precise matching
+
+---
+
+### Filter Configuration Guide
+
+This section covers content-level filtering at the Sumo Logic source level using `log_source_filters` and `activity_log_filters`.
+
+#### Filter Types and Behavior
+
+| Filter Type | Behavior | When to Use |
+|-------------|----------|-------------|
+| **Include** | Only matching logs ingested; all others dropped | Reduce data volume - collect only errors, specific operations, or critical resources |
+| **Exclude** | Matching logs dropped; all others ingested | Remove noise - exclude health checks, debug logs, or test traffic |
+| **Mask** | Matching content replaced with `mask` string | Redact sensitive data - mask PII, credentials, API keys, credit cards |
+
+**Evaluation Order:**
+- Filters process **in definition order**; **first match wins**
+- If Include filters exist and log doesn't match any: **dropped**
+- If only Exclude/Mask filters exist and log doesn't match: **ingested**
+
+**Example:**
+```hcl
+log_source_filters = [
+ { filter_type = "Include", regexp = ".*\"level\":\"error\".*", ... }, # Evaluated first
+ { filter_type = "Mask", regexp = "(\\\"password\\\".*)", ... } # Evaluated second
+]
+# Result: Only error logs ingested. Password masking never applies to info logs (dropped before reaching mask filter).
+```
+
+#### Common Filter Issues
+
+| Issue | Cause | Solution |
+|-------|-------|----------|
+| **Filters not applied / All logs ingested** | Regex doesn't match log message format | Azure diagnostic logs are JSON strings. Ensure your regex matches the **entire message** using `.*` prefix/suffix. Test with sample logs. |
+| **No logs ingested (all dropped)** | Include filter too restrictive or regex error | Check regex syntax. Verify pattern matches expected log structure. Use `terraform plan` to review filter configuration. |
+| **Mask filter validation error: "mask string should not contain any colons"** | Sumo Logic API rejects mask strings with colons (`:`) | Remove all colons from `mask` value. Use alternatives like `***REDACTED***`, `MASKED_VALUE`, or `****`. |
+| **Region-specific filter applies to all regions** | Missing trailing comma in HCL object list with optional fields | Add trailing comma after `regions` field: `regions = ["East US"],` (note comma before closing brace) |
+| **Filter only applies to some regions, not all** | Empty `regions` list not specified or HCL syntax error | Explicitly set `regions = []` or omit the field entirely. Ensure trailing comma if present. |
+| **Masked content still visible in logs** | Regex doesn't capture the sensitive portion | Use capture groups `()` in regex. The mask replaces the **captured content**, not the entire match. Example: `"(\\d{16})"` captures card number. |
+| **Filters work for log sources but not metrics sources** | Metrics sources don't support content filters | Content filtering (`log_source_filters`) only applies to Event Hub log sources. For metrics, use `azure_tag_filters` (resource-level filtering). |
+
+#### Filter Regex Patterns Guide
+
+Azure diagnostic logs are JSON formatted. Your regex must match the complete message.
+
+**Key Principles:**
+- Use `.*` prefix/suffix for partial matching (e.g., `".*\"level\":\"error\".*"`)
+- Escape JSON special chars: `\"`, `\{`, `\:`
+- RE2 syntax (Google's regex library)
+- Test patterns before deploying
+
+**Common Patterns:**
+
+
+Match JSON Fields
+
+```hcl
+# Operation names with "write"
+regexp = ".*\"operationName\"\\s*:\\s*\"[^\"]*write[^\"]*\".*"
+
+# Result types
+regexp = ".*\"ResultType\"\\s*:\\s*\"Success\".*"
+
+# Severity levels
+regexp = ".*\"severity\"\\s*:\\s*\"(Error|Critical)\".*"
+
+# Resource IDs
+regexp = ".*\"resourceId\"\\s*:\\s*\"[^\"]*SBUS002[^\"]*\".*"
+```
+
+
+
+Mask Sensitive Data
+
+```hcl
+# Password fields
+regexp = "(\\\"password\\\"\\s*:\\s*\\\"[^\\\"]+\\\")"
+mask = "***REDACTED***"
+
+# Credit cards (16 digits)
+regexp = "(\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4})"
+mask = "XXXX-XXXX-XXXX-XXXX"
+
+# Email addresses
+regexp = "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})"
+mask = "***EMAIL***"
+
+# IP addresses
+regexp = "(\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b)"
+mask = "***IP***"
+```
+
+
+
+Include/Exclude Patterns
+
+```hcl
+# Include only errors and warnings
+regexp = ".*\"level\"\\s*:\\s*\"(error|warning)\".*"
+
+# Exclude health checks
+regexp = ".*healthcheck.*"
+
+# Include write operations only
+regexp = ".*\"operationName\"\\s*:\\s*\"[^\"]*write[^\"]*\".*"
+```
+
+
+**Test Your Regex:**
+```bash
+# Online tester: https://regex101.com/ (select "Golang" flavor)
+# Or verify in Terraform plan:
+terraform plan 2>&1 | grep -A 10 "filters {"
+```
+
+#### Filter Regional Behavior
+
+Control where filters apply using the `regions` field in `log_source_filters`.
+
+**Behavior:**
+- **Region matching**: Case-insensitive, spaces removed (`"East US"` = `"eastus"`)
+- **Empty list** (`[]`) or **omitted**: Applies to **all regions**
+- **Specific regions**: `["East US", "West US"]` - applies only to those regions
+- **Not available**: Activity logs (subscription-level, no regions field)
+
+**Examples:**
+
+
+Different Filters Per Region
+
+```hcl
+log_source_filters = [
+ # Production: Strict filtering (East US only)
+ { filter_type = "Include", regexp = ".*\"level\":\"error\".*", regions = ["East US"] },
+
+ # Development: All logs (West US only)
+ { filter_type = "Include", regexp = ".*", regions = ["West US"] },
+
+ # Global: Mask passwords everywhere
+ { filter_type = "Mask", regexp = "(\\\"password\\\".*)", mask = "***REDACTED***" }
+]
+```
+
+
+
+Compliance-Specific Filtering
+
+```hcl
+log_source_filters = [
+ # EU GDPR: Extra PII masking
+ { filter_type = "Mask", regexp = "([a-zA-Z0-9._%+-]+@[^\"]+)", mask = "***EMAIL***",
+ regions = ["North Europe", "West Europe"] },
+
+ # US: Include audit events
+ { filter_type = "Include", regexp = ".*\"category\":\"AuditEvent\".*",
+ regions = ["East US", "West US", "Central US"] }
+]
+```
+
+
+**Verify:**
+```bash
+# Check resource regions
+az resource list --query "[].location" -o tsv | sort -u
+
+# Verify filter distribution in plan
+terraform plan 2>&1 | grep -B 5 "filters {"
+```
+
+#### Filter Mask Validation
+
+**Rules:**
+- ✅ Allowed: Alphanumeric, spaces, underscores, hyphens, asterisks, periods
+- ❌ **Not Allowed: Colons (`:`)** - causes API error
+
+**Common Errors:**
+
+| Invalid | Error | Fixed |
+|---------|-------|-------|
+| `"password":"***"` | Contains colon | `***REDACTED***` |
+| `"masked:value"` | Contains colon | `masked_value` |
+
+**Fix:**
+```hcl
+# ❌ WRONG
+mask = "\"password\":\"***REDACTED***\""
+
+# ✅ CORRECT
+mask = "***REDACTED***"
+```
+
+---
+
+### Debugging
+
+Enable detailed logs:
+```bash
+# Terraform debugging
+export TF_LOG=DEBUG
+terraform apply
+
+# Azure CLI debugging
+az account show --debug
+```
+
+**Check EventHub metrics in Azure Portal:**
+- Navigate to: Monitor → Metrics → Select EventHub namespace
+- View "Incoming Messages" and "Outgoing Messages" to verify data flow
+
+**Verify Terraform resource discovery:**
+```bash
+# See which resources will be configured
+terraform plan 2>&1 | grep -A 5 "azurerm_monitor_diagnostic_setting"
+
+# Check data source outputs
+terraform plan 2>&1 | grep "data.azurerm_resources"
+```
+
+For tag filtering verification, see the [Resource Tag Filtering Guide](#resource-tag-filtering-guide) section above.
+
+## Cleanup
+
+To destroy all resources:
+
+```bash
+terraform destroy
+```
+
+### Automated Lock Handling
+
+This module automatically removes Azure resource locks before deleting diagnostic settings to prevent "ScopeLocked" errors.
+
+**Requirements:**
+- Azure CLI authentication (`az login`)
+- **Owner** or **User Access Administrator** role to remove locks
+
+**Note:** Only locks on resources with diagnostic settings are checked and removed. Other resources remain unaffected.
+
+### ⚠️ Warning
+
+This will:
+- **Automatically remove Azure resource locks** (subscription, resource group, and resource-level)
+- Delete all EventHub namespaces and hubs
+- Remove diagnostic settings from Azure resources
+- Delete Sumo Logic collector and all sources
+- Remove installed Sumo Logic apps
+- **Remove subscription-level activity log diagnostic settings** (if `enable_activity_logs = true`)
+ - This affects the **ENTIRE subscription**, not just resources managed by this Terraform configuration
+ - Other monitoring solutions or Terraform workspaces relying on activity logs will be impacted
+
+**💡 To safely remove activity logs before destroying:**
+```bash
+# Step 1: Disable activity logs in your terraform.tfvars
+# Change: enable_activity_logs = true
+# To: enable_activity_logs = false
+
+# Step 2: Apply the change (this removes only activity logs, keeps everything else)
+terraform apply
+
+# Step 3: Now safely destroy the rest
+terraform destroy
+```
+
+Data in Sumo Logic will be retained based on your retention policy.
\ No newline at end of file
diff --git a/azure-collection-terraform/azure_resources.tf b/azure-collection-terraform/azure_resources.tf
new file mode 100644
index 00000000..8dc6c2eb
--- /dev/null
+++ b/azure-collection-terraform/azure_resources.tf
@@ -0,0 +1,340 @@
+# Query resources with per-resource-type tag filtering (supports AND logic)
+data "azurerm_resources" "target_resources_by_type" {
+ for_each = {
+ for config in var.target_resource_types :
+ coalesce(config.log_namespace, config.metric_namespace) => config.required_resource_tags
+ if coalesce(config.log_namespace, config.metric_namespace) != null
+ }
+
+ type = each.key
+ required_tags = each.value
+}
+
+data "azurerm_client_config" "current" {}
+
+data "azurerm_monitor_diagnostic_categories" "all_categories" {
+ for_each = local.all_monitored_resources
+ resource_id = each.value.id
+}
+
+# Get diagnostic categories for each resource type
+data "azurerm_monitor_diagnostic_categories" "type_categories" {
+ for_each = {
+ for type, resource_id in local.resource_type_examples :
+ type => resource_id
+ if resource_id != null
+ }
+ resource_id = each.value
+}
+
+resource "azurerm_resource_group" "rg" {
+ name = var.resource_group_name
+ location = var.location
+}
+
+resource "azurerm_eventhub_namespace" "namespaces_by_location" {
+ #checkov:skip=CKV_AZURE_228:Zone redundancy is automatically enabled by Azure for Event Hub namespaces in regions with availability zones. There is no explicit configuration property in the Azure API or Terraform provider - it's implicit based on region support.
+ for_each = local.resources_by_location_only
+
+ name = "${var.eventhub_namespace_name}-${replace(lower(each.key), " ", "")}"
+ location = each.key
+ resource_group_name = azurerm_resource_group.rg.name
+ sku = local.eventhub_sku_by_region[each.key].sku
+ capacity = local.eventhub_sku_by_region[each.key].throughput_units
+
+ tags = {
+ version = local.solution_version
+ }
+}
+
+resource "azurerm_eventhub" "eventhubs_by_type_and_location" {
+ for_each = {
+ for k, v in local.resources_by_type_and_location : k => v
+ if length([
+ for config in var.target_resource_types :
+ config if config.log_namespace == local.eventhub_key_to_log_namespace[k] &&
+ config.log_namespace != null &&
+ config.log_namespace != ""
+ ]) > 0
+ }
+
+ name = lower("eventhub-${replace(each.key, "/", "-")}")
+ namespace_id = azurerm_eventhub_namespace.namespaces_by_location[each.value[0].location].id
+ partition_count = 4
+ # Basic SKU only supports 1 day retention; Standard/Premium/Dedicated support up to 7 days
+ message_retention = local.eventhub_sku_by_region[each.value[0].location].sku == "Basic" ? 1 : 7
+}
+
+resource "null_resource" "verify_all_eventhubs_exist" {
+ provisioner "local-exec" {
+ command = <<-EOT
+ #!/bin/bash
+ set -e
+ RG_NAME="${var.resource_group_name}"
+ MAX_RETRIES=30
+ RETRY_INTERVAL=10
+
+ EVENTHUBS='${jsonencode([for k, v in azurerm_eventhub.eventhubs_by_type_and_location : {
+ namespace = split("/", v.namespace_id)[8]
+ name = v.name
+ }])}'
+
+ echo "========================================="
+ echo "Verifying ALL Event Hubs exist in Azure backend before diagnostic settings..."
+ echo "========================================="
+ echo "$EVENTHUBS" | jq -r '.[] | " - \(.namespace)/\(.name)"'
+ echo ""
+
+ echo "$EVENTHUBS" | jq -r '.[] | "\(.namespace) \(.name)"' | while read NAMESPACE_NAME EVENTHUB_NAME; do
+ echo "Checking: $EVENTHUB_NAME in $NAMESPACE_NAME"
+
+ for i in $(seq 1 $MAX_RETRIES); do
+ if az eventhubs eventhub show \
+ --resource-group "$RG_NAME" \
+ --namespace-name "$NAMESPACE_NAME" \
+ --name "$EVENTHUB_NAME" \
+ --query "id" -o tsv 2>/dev/null | grep -q "eventhubs/$EVENTHUB_NAME"; then
+ echo " ✓ Visible (attempt $i/$MAX_RETRIES)"
+ break
+ fi
+
+ if [ $i -eq $MAX_RETRIES ]; then
+ echo " ✗ ERROR: Not visible after $((MAX_RETRIES * RETRY_INTERVAL)) seconds"
+ exit 1
+ fi
+
+ echo " ⏳ Waiting... (attempt $i/$MAX_RETRIES)"
+ sleep $RETRY_INTERVAL
+ done
+ done
+
+ echo ""
+ echo "✅ All Event Hubs verified and visible in Azure backend"
+ echo "========================================="
+ EOT
+ }
+
+ depends_on = [azurerm_eventhub.eventhubs_by_type_and_location]
+
+ triggers = {
+ eventhub_ids = jsonencode(keys(azurerm_eventhub.eventhubs_by_type_and_location))
+ }
+}
+
+resource "azurerm_eventhub_namespace_authorization_rule" "sumo_collection_policy" {
+ for_each = azurerm_eventhub_namespace.namespaces_by_location
+
+ name = var.policy_name
+ namespace_name = each.value.name
+ resource_group_name = azurerm_resource_group.rg.name
+ listen = true
+ send = true
+ manage = false
+}
+
+data "external" "existing_diagnostic_settings" {
+ for_each = local.all_monitored_resources
+
+ program = ["bash", "-c", <<-EOT
+ RESOURCE_ID="${each.value.id}"
+ EXISTING=$(az monitor diagnostic-settings list --resource "$RESOURCE_ID" --query "value[?name=='diag-${replace(replace(each.value.name, "/", "-"), ".", "-")}'].id" -o tsv 2>/dev/null || echo "")
+ if [ -n "$EXISTING" ]; then
+ echo '{"exists":"true","id":"'"$EXISTING"'"}'
+ else
+ echo '{"exists":"false","id":""}'
+ fi
+ EOT
+ ]
+}
+
+resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_logs" {
+ # Fail early if any log categories are invalid
+ lifecycle {
+ precondition {
+ condition = length(local.log_category_validation_errors) == 0
+ error_message = "Invalid log categories detected:\n${join("\n", local.log_category_validation_errors)}"
+ }
+ }
+
+ for_each = {
+ for k, v in local.all_monitored_resources : k => v
+ if !contains(local.unsupported_eventhub_locations, lower(replace(v.location, " ", ""))) &&
+ length([
+ for config in var.target_resource_types :
+ config if config.log_namespace == lookup(v, "parent_type", v.type) &&
+ config.log_namespace != null &&
+ config.log_namespace != ""
+ ]) > 0 &&
+ lookup(data.external.existing_diagnostic_settings[k].result, "exists", "false") == "false"
+ }
+
+ name = "diag-${replace(replace(each.value.name, "/", "-"), ".", "-")}"
+ target_resource_id = each.value.id
+ eventhub_authorization_rule_id = azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy[each.value.location].id
+
+ eventhub_name = azurerm_eventhub.eventhubs_by_type_and_location[
+ "${each.value.type}-${each.value.location}"
+ ].name
+
+ dynamic "enabled_log" {
+ # Select categories to enable: use the union of configured `log_categories` for the resource's
+ # log_namespace when provided; otherwise enable all available categories returned by Azure.
+ for_each = (
+ length([
+ for config in var.target_resource_types :
+ config if config.log_namespace == lookup(each.value, "parent_type", each.value.type)
+ ]) > 0
+ ? (
+ length([
+ for config in var.target_resource_types :
+ config if config.log_namespace == lookup(each.value, "parent_type", each.value.type) && length(lookup(config, "log_categories", [])) > 0
+ ]) > 0
+ ? distinct(flatten([
+ for config in var.target_resource_types :
+ lookup(config, "log_categories", []) if config.log_namespace == lookup(each.value, "parent_type", each.value.type)
+ ]))
+ : data.azurerm_monitor_diagnostic_categories.all_categories[each.key].log_category_types
+ )
+ : []
+ )
+ content {
+ category = enabled_log.value
+ }
+ }
+
+ dynamic "enabled_metric" {
+ for_each = length(data.azurerm_monitor_diagnostic_categories.all_categories[each.key].log_category_types) == 0 ? data.azurerm_monitor_diagnostic_categories.all_categories[each.key].metrics : []
+ content {
+ category = enabled_metric.value
+ }
+ }
+
+ depends_on = [
+ azurerm_eventhub_namespace.namespaces_by_location,
+ azurerm_eventhub.eventhubs_by_type_and_location,
+ azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy,
+ null_resource.verify_all_eventhubs_exist
+ ]
+
+ timeouts {
+ create = "30m"
+ update = "30m"
+ delete = "60m"
+ }
+}
+
+resource "azurerm_eventhub_namespace" "activity_logs_namespace" {
+ #checkov:skip=CKV_AZURE_228:Zone redundancy is automatically enabled by Azure for Event Hub namespaces in regions with availability zones. There is no explicit configuration property in the Azure API or Terraform provider - it's implicit based on region support.
+ count = var.enable_activity_logs && !(contains(local.unsupported_eventhub_locations, lower(replace(var.location, " ", "")))) ? 1 : 0
+ name = "${var.eventhub_namespace_name}-activity-logs"
+ location = var.location
+ resource_group_name = azurerm_resource_group.rg.name
+ sku = local.eventhub_sku_by_region[var.location].sku
+ capacity = local.eventhub_sku_by_region[var.location].throughput_units
+}
+
+resource "azurerm_eventhub_namespace_authorization_rule" "activity_logs_policy" {
+ count = var.enable_activity_logs && !(contains(local.unsupported_eventhub_locations, lower(replace(var.location, " ", "")))) ? 1 : 0
+ name = var.policy_name
+ namespace_name = azurerm_eventhub_namespace.activity_logs_namespace[0].name
+ resource_group_name = azurerm_resource_group.rg.name
+ listen = true
+ send = true
+ manage = false
+}
+
+resource "azurerm_eventhub" "eventhub_for_activity_logs" {
+ count = var.enable_activity_logs && !(contains(local.unsupported_eventhub_locations, lower(replace(var.location, " ", "")))) ? 1 : 0
+ name = var.activity_log_export_name
+ namespace_id = azurerm_eventhub_namespace.activity_logs_namespace[0].id
+ partition_count = 4
+ # Basic SKU only supports 1 day retention; Standard/Premium/Dedicated support up to 7 days
+ message_retention = local.eventhub_sku_by_region[var.location].sku == "Basic" ? 1 : 7
+}
+
+resource "null_resource" "verify_activity_log_eventhub" {
+ count = var.enable_activity_logs && !(contains(local.unsupported_eventhub_locations, lower(replace(var.location, " ", "")))) ? 1 : 0
+
+ provisioner "local-exec" {
+ command = <<-EOT
+ #!/bin/bash
+ set -e
+ NAMESPACE_NAME="${azurerm_eventhub_namespace.activity_logs_namespace[0].name}"
+ EVENTHUB_NAME="${azurerm_eventhub.eventhub_for_activity_logs[0].name}"
+ RG_NAME="${var.resource_group_name}"
+ MAX_RETRIES=30
+ RETRY_INTERVAL=10
+
+ echo "Verifying Activity Log Event Hub exists in Azure backend: $EVENTHUB_NAME"
+
+ for i in $(seq 1 $MAX_RETRIES); do
+ if az eventhubs eventhub show \
+ --resource-group "$RG_NAME" \
+ --namespace-name "$NAMESPACE_NAME" \
+ --name "$EVENTHUB_NAME" \
+ --query "id" -o tsv 2>/dev/null | grep -q "eventhubs/$EVENTHUB_NAME"; then
+ echo "✓ Activity Log Event Hub is visible in Azure backend (attempt $i/$MAX_RETRIES)"
+ exit 0
+ fi
+ echo " Waiting for Activity Log Event Hub to propagate (attempt $i/$MAX_RETRIES)..."
+ sleep $RETRY_INTERVAL
+ done
+
+ echo "ERROR: Activity Log Event Hub not visible after $((MAX_RETRIES * RETRY_INTERVAL)) seconds"
+ exit 1
+ EOT
+ }
+
+ depends_on = [azurerm_eventhub.eventhub_for_activity_logs]
+
+ triggers = {
+ eventhub_id = var.enable_activity_logs ? azurerm_eventhub.eventhub_for_activity_logs[0].id : ""
+ }
+}
+
+resource "azurerm_monitor_diagnostic_setting" "activity_logs_to_event_hub" {
+ count = var.enable_activity_logs && !(contains(local.unsupported_eventhub_locations, lower(replace(var.location, " ", "")))) ? 1 : 0
+ name = var.activity_log_export_name
+ target_resource_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}"
+ eventhub_name = azurerm_eventhub.eventhub_for_activity_logs[0].name
+ eventhub_authorization_rule_id = azurerm_eventhub_namespace_authorization_rule.activity_logs_policy[0].id
+
+ enabled_log {
+ category = "Administrative"
+ }
+ enabled_log {
+ category = "Security"
+ }
+ enabled_log {
+ category = "ServiceHealth"
+ }
+ enabled_log {
+ category = "Alert"
+ }
+ enabled_log {
+ category = "Recommendation"
+ }
+ enabled_log {
+ category = "Policy"
+ }
+ enabled_log {
+ category = "Autoscale"
+ }
+ enabled_log {
+ category = "ResourceHealth"
+ }
+
+ depends_on = [
+ azurerm_eventhub_namespace.activity_logs_namespace,
+ azurerm_eventhub.eventhub_for_activity_logs,
+ azurerm_eventhub_namespace_authorization_rule.activity_logs_policy,
+ null_resource.verify_activity_log_eventhub,
+ null_resource.verify_all_eventhubs_exist
+ ]
+
+ timeouts {
+ create = "30m"
+ update = "30m"
+ delete = "30m"
+ }
+}
\ No newline at end of file
diff --git a/azure-collection-terraform/import.tf b/azure-collection-terraform/import.tf
new file mode 100644
index 00000000..e69de29b
diff --git a/azure-collection-terraform/locals.tf b/azure-collection-terraform/locals.tf
new file mode 100644
index 00000000..8a1630e3
--- /dev/null
+++ b/azure-collection-terraform/locals.tf
@@ -0,0 +1,241 @@
+locals {
+ solution_version = "v1.0.0"
+
+ # Normalize region-specific SKU overrides (lowercase, no spaces) for lookup
+ normalized_region_skus = {
+ for region, config in var.region_specific_eventhub_skus :
+ lower(replace(region, " ", "")) => config
+ }
+
+ # Get valid categories from Azure for each unique resource type
+ # Use coalesce to match the data source logic: prefer log_namespace, fall back to metric_namespace
+ unique_resource_types = distinct([
+ for config in var.target_resource_types :
+ coalesce(config.log_namespace, config.metric_namespace)
+ if coalesce(config.log_namespace, config.metric_namespace) != null &&
+ coalesce(config.log_namespace, config.metric_namespace) != ""
+ ])
+
+ # We need at least one resource of each type to get valid categories
+ # Use the new per-type data source
+ resource_type_examples = {
+ for type in local.unique_resource_types :
+ type => try(
+ data.azurerm_resources.target_resources_by_type[type].resources[0].id,
+ null
+ )
+ }
+
+ # Get diagnostic categories for each resource type using the example resources
+ valid_log_categories_by_type = {
+ for type, resource_id in local.resource_type_examples :
+ type => resource_id != null ? try(
+ data.azurerm_monitor_diagnostic_categories.type_categories[type].log_category_types,
+ []
+ ) : []
+ }
+
+ # Resource types that need category validation (have non-empty log_categories)
+ resources_needing_validation = toset([
+ for config in var.target_resource_types :
+ config.log_namespace
+ if config.log_namespace != null &&
+ config.log_categories != null &&
+ length(config.log_categories) > 0
+ ])
+
+ # Get valid categories for each resource type that needs validation
+ # Uses the efficient type_categories data source (one query per resource type)
+ valid_categories_by_resource = {
+ for type in local.resources_needing_validation :
+ type => try(
+ data.azurerm_monitor_diagnostic_categories.type_categories[type].log_category_types,
+ []
+ )
+ }
+
+ # Validate log categories and collect any errors
+ # Skip validation if no resources found for the type (after tag filtering)
+ log_category_validation_errors = flatten([
+ for config in var.target_resource_types : [
+ for category in coalesce(config.log_categories, []) :
+ "Invalid category '${category}' for resource type '${config.log_namespace}'. Valid categories are: ${join(", ", try(local.valid_categories_by_resource[config.log_namespace], []))}"
+ if config.log_namespace != null &&
+ length(coalesce(config.log_categories, [])) > 0 &&
+ try(local.resource_type_examples[config.log_namespace], null) != null && # Only validate if resources exist
+ !contains(try(local.valid_categories_by_resource[config.log_namespace], []), category)
+ ]
+ ])
+
+ log_namespaces = distinct([
+ for config in var.target_resource_types : config.log_namespace
+ if config.log_namespace != null && config.log_namespace != ""
+ ])
+
+ metric_namespaces = distinct([
+ for config in var.target_resource_types : config.metric_namespace
+ if config.metric_namespace != null && config.metric_namespace != ""
+ ])
+
+ # Combined list: prioritize log_namespace, but include metric_namespace if log_namespace is missing
+ resource_types_for_discovery = distinct(concat(
+ local.log_namespaces,
+ [
+ for config in var.target_resource_types : config.metric_namespace
+ if config.metric_namespace != null && config.metric_namespace != "" &&
+ (config.log_namespace == null || config.log_namespace == "")
+ ]
+ ))
+
+ namespace_mapping = {
+ for config in var.target_resource_types :
+ config.log_namespace => config.metric_namespace
+ if config.log_namespace != null && config.log_namespace != ""
+ }
+
+ # Normalized list of unsupported regions for Event Hub namespace creation.
+ # We normalize by lower-casing and removing spaces so inputs like "East US" and "eastus" match.
+ unsupported_eventhub_locations = [for loc in var.eventhub_namespace_unsupported_locations : lower(replace(loc, " ", ""))]
+
+ # Normalized list of locations that only support Basic/Standard SKUs for Event Hub namespaces
+ limited_eventhub_sku_locations = [for loc in var.eventhub_namespace_limited_sku_locations : lower(replace(loc, " ", ""))]
+
+ metric_to_log_mapping = {
+ for config in var.target_resource_types :
+ config.metric_namespace => config.log_namespace
+ if config.metric_namespace != null && config.metric_namespace != ""
+ }
+
+ parents_with_nested_configs = keys(var.nested_namespace_configs)
+
+ # Map of resource type to name_filter (regex pattern) for filtering
+ type_to_name_filter = {
+ for config in var.target_resource_types :
+ coalesce(config.log_namespace, config.metric_namespace) => lookup(config, "name_filter", "")
+ if coalesce(config.log_namespace, config.metric_namespace) != null
+ }
+
+ # Flatten resources using per-type data source (no distinct needed, will dedupe by ID in map)
+ all_resources_list = flatten([
+ # Non-nested resources with name_filter (regex) filtering
+ [for type in local.resource_types_for_discovery : [
+ for res in data.azurerm_resources.target_resources_by_type[type].resources : res
+ if !contains(local.parents_with_nested_configs, type) &&
+ (local.type_to_name_filter[type] == "" || can(regex(local.type_to_name_filter[type], lower(res.name))))
+ ]],
+ # Nested resources
+ [for parent_type, children_types in var.nested_namespace_configs : [
+ for parent_res in(
+ contains(local.resource_types_for_discovery, parent_type) ?
+ data.azurerm_resources.target_resources_by_type[parent_type].resources : []
+ ) : [
+ for child_type in children_types : {
+ id = "${parent_res.id}/${element(split("/", child_type), length(split("/", child_type)) - 1)}/default"
+ name = "${parent_res.name}/${element(split("/", child_type), length(split("/", child_type)) - 1)}/default"
+ type = child_type
+ location = parent_res.location
+ resource_group_name = parent_res.resource_group_name
+ parent_resource_id = parent_res.id
+ parent_type = parent_type
+ }
+ ]
+ ]]
+ ])
+
+ all_monitored_resources = { for res in local.all_resources_list : res.id => res }
+
+ resources_by_location_only = {
+ for res in values(local.all_monitored_resources) :
+ res.location => res...
+ if !contains(local.unsupported_eventhub_locations, lower(replace(res.location, " ", "")))
+ }
+
+ resources_by_type_and_location = {
+ for res in values(local.all_monitored_resources) :
+ "${res.type}-${res.location}" => res...
+ if !contains(local.unsupported_eventhub_locations, lower(replace(res.location, " ", "")))
+ }
+
+ eventhub_key_to_log_namespace_grouped = {
+ for res in values(local.all_monitored_resources) :
+ "${res.type}-${res.location}" => lookup(res, "parent_type", res.type)...
+ }
+
+ eventhub_key_to_log_namespace = {
+ for k, v in local.eventhub_key_to_log_namespace_grouped : k => v[0]
+ }
+
+ metrics_source_groups = {
+ for config in var.target_resource_types :
+ config.metric_namespace => {
+ namespaces = config.log_namespace != null && config.log_namespace != "" ? (
+ lookup(var.nested_namespace_configs, config.log_namespace, [config.metric_namespace])
+ ) : [config.metric_namespace]
+
+ enabled = config.metric_namespace != null && config.metric_namespace != ""
+
+ regions = [distinct([
+ for res in values(local.all_monitored_resources) :
+ replace(res.location, " ", "")
+ if config.log_namespace != null && config.log_namespace != "" ? (
+ res.type == config.log_namespace || lookup(res, "parent_type", "") == config.log_namespace
+ ) : (
+ res.type == config.metric_namespace
+ )
+ ])]
+
+ tag_filters = length(config.required_resource_tags) > 0 ? [{
+ type = "AzureTagFilters"
+ namespace = config.metric_namespace
+ tags = [
+ for tag_key, tag_value in config.required_resource_tags : {
+ name = tag_key
+ values = [tag_value]
+ }
+ ]
+ }] : []
+ }
+ if config.metric_namespace != null && config.metric_namespace != ""
+ }
+
+ eventhub_count_by_location = {
+ for location in keys(local.resources_by_location_only) :
+ location => length([
+ for k, v in local.resources_by_type_and_location :
+ k if v[0].location == location && length([
+ for config in var.target_resource_types :
+ config if config.log_namespace == local.eventhub_key_to_log_namespace[k] &&
+ config.log_namespace != null &&
+ config.log_namespace != ""
+ ]) > 0
+ ])
+ }
+
+ max_possible_eventhubs_per_config = length([
+ for config in var.target_resource_types :
+ config.log_namespace
+ if config.log_namespace != null && config.log_namespace != ""
+ ])
+
+ eventhub_sku_by_region = {
+ for location in distinct(concat(
+ [for res in values(local.all_monitored_resources) : res.location if !contains(local.unsupported_eventhub_locations, lower(replace(res.location, " ", "")))],
+ var.enable_activity_logs && !(contains(local.unsupported_eventhub_locations, lower(replace(var.location, " ", "")))) ? [var.location] : []
+ )) :
+ location => (
+ contains(keys(local.normalized_region_skus), lower(replace(location, " ", ""))) ?
+ local.normalized_region_skus[lower(replace(location, " ", ""))] :
+ {
+ sku = (
+ contains(local.limited_eventhub_sku_locations, lower(replace(location, " ", ""))) ? "Standard" :
+ max(
+ lookup(local.eventhub_count_by_location, location, 0),
+ local.max_possible_eventhubs_per_config
+ ) >= 9 && contains(["Basic", "Standard"], var.eventhub_namespace_sku) ? "Premium" :
+ var.eventhub_namespace_sku
+ )
+ throughput_units = var.default_throughput_units
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/azure-collection-terraform/lock_handler.tf b/azure-collection-terraform/lock_handler.tf
new file mode 100644
index 00000000..b888d7fa
--- /dev/null
+++ b/azure-collection-terraform/lock_handler.tf
@@ -0,0 +1,259 @@
+# Lock Handler Resource
+# This resource detects and removes Azure resource locks BEFORE attempting to delete
+# diagnostic settings. This prevents "ScopeLocked" errors when diagnostic settings
+# reference locked resources (e.g., storage accounts).
+#
+# Triggers when diagnostic settings are being removed (not just on destroy)
+
+resource "null_resource" "lock_handler" {
+ # Trigger whenever the set of diagnostic settings changes
+ # This ensures locks are removed before diagnostic settings deletion
+ triggers = {
+ diagnostic_settings = jsonencode(keys(azurerm_monitor_diagnostic_setting.diagnostic_setting_logs))
+ }
+
+ # Check and remove locks BEFORE any changes (create/update/destroy)
+ provisioner "local-exec" {
+ command = <<-SCRIPT
+ #!/bin/bash
+ set -e
+
+ echo "==================== Azure Lock Handler (Pre-Change) ===================="
+ echo "Checking for locks on resources with diagnostic settings..."
+
+ # Get subscription ID from Azure CLI
+ SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null || echo "")
+
+ if [ -z "$SUBSCRIPTION_ID" ]; then
+ echo "⚠️ Warning: Unable to detect Azure subscription. Skipping lock check."
+ echo "Make sure you're logged in with: az login"
+ exit 0
+ fi
+
+ echo "✓ Detected subscription: $SUBSCRIPTION_ID"
+
+ # Get list of diagnostic settings from current state
+ echo ""
+ echo "Identifying resources with diagnostic settings..."
+
+ DIAG_SETTINGS=$(terraform state list 2>/dev/null | grep 'azurerm_monitor_diagnostic_setting.diagnostic_setting_logs' || echo "")
+
+ if [ -z "$DIAG_SETTINGS" ]; then
+ echo "✓ No diagnostic settings found in state - no locks to check"
+ exit 0
+ fi
+
+ LOCKS_FOUND=0
+ LOCKS_REMOVED=0
+
+ # For each diagnostic setting, extract the target resource ID and check for locks
+ echo "$DIAG_SETTINGS" | while read DIAG_SETTING_KEY; do
+ if [ -n "$DIAG_SETTING_KEY" ]; then
+ # Get the target resource ID from the diagnostic setting
+ TARGET_RESOURCE_ID=$(terraform state show "$DIAG_SETTING_KEY" 2>/dev/null | grep 'target_resource_id' | head -1 | awk '{print $3}' | tr -d '"' || echo "")
+
+ if [ -n "$TARGET_RESOURCE_ID" ]; then
+ # Extract resource type, name, and resource group from the ID
+ RESOURCE_TYPE=$(echo "$TARGET_RESOURCE_ID" | awk -F'/providers/' '{print $2}' | awk -F'/' '{print $1"/"$2}')
+ RESOURCE_NAME=$(echo "$TARGET_RESOURCE_ID" | awk -F'/' '{print $NF}')
+ RESOURCE_GROUP=$(echo "$TARGET_RESOURCE_ID" | awk -F'/resourceGroups/' '{print $2}' | awk -F'/' '{print $1}')
+
+ # Handle nested resources (e.g., storage account services)
+ if echo "$TARGET_RESOURCE_ID" | grep -q "storageAccounts.*Services"; then
+ # Extract parent storage account info
+ PARENT_RESOURCE_ID=$(echo "$TARGET_RESOURCE_ID" | sed 's|/blobServices/.*||; s|/fileServices/.*||; s|/queueServices/.*||; s|/tableServices/.*||')
+ RESOURCE_TYPE="Microsoft.Storage/storageAccounts"
+ RESOURCE_NAME=$(echo "$PARENT_RESOURCE_ID" | awk -F'/storageAccounts/' '{print $2}' | awk -F'/' '{print $1}')
+ fi
+
+ if [ -n "$RESOURCE_GROUP" ] && [ -n "$RESOURCE_NAME" ] && [ -n "$RESOURCE_TYPE" ]; then
+ # Check for locks on this specific resource
+ RESOURCE_LOCKS=$(az lock list \
+ --resource-group "$RESOURCE_GROUP" \
+ --resource-name "$RESOURCE_NAME" \
+ --resource-type "$RESOURCE_TYPE" \
+ --query "[].{name:name, id:id, level:level}" \
+ -o json 2>/dev/null || echo "[]")
+
+ if [ "$RESOURCE_LOCKS" != "[]" ] && [ -n "$RESOURCE_LOCKS" ]; then
+ LOCK_COUNT=$(echo "$RESOURCE_LOCKS" | jq 'length')
+ LOCKS_FOUND=$((LOCKS_FOUND + LOCK_COUNT))
+
+ echo ""
+ echo "⚠️ Found $LOCK_COUNT lock(s) on: $RESOURCE_NAME ($RESOURCE_TYPE)"
+ echo "$RESOURCE_LOCKS" | jq -r '.[] | " - \(.name) (\(.level))"'
+
+ # Remove each lock
+ echo "$RESOURCE_LOCKS" | jq -r '.[].id' | while read LOCK_ID; do
+ if [ -n "$LOCK_ID" ]; then
+ LOCK_NAME=$(echo "$RESOURCE_LOCKS" | jq -r ".[] | select(.id==\"$LOCK_ID\") | .name")
+ echo " → Removing lock: $LOCK_NAME"
+ if az lock delete --ids "$LOCK_ID" 2>/dev/null; then
+ echo " ✓ Lock removed successfully"
+ LOCKS_REMOVED=$((LOCKS_REMOVED + 1))
+ else
+ echo " ⚠️ Failed to remove lock (may lack permissions)"
+ fi
+ fi
+ done
+ fi
+
+ # Also check for locks at the resource group level (these also block child resource operations)
+ RG_LOCKS=$(az lock list \
+ --resource-group "$RESOURCE_GROUP" \
+ --query "[?!contains(id, '/providers/Microsoft.Storage/') && !contains(id, '/providers/Microsoft.Network/') && !contains(id, '/providers/Microsoft.Compute/') && contains(id, '/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Authorization/locks')].{name:name, id:id, level:level}" \
+ -o json 2>/dev/null || echo "[]")
+
+ if [ "$RG_LOCKS" != "[]" ] && [ -n "$RG_LOCKS" ] && [ "$(echo "$RG_LOCKS" | jq 'length')" -gt 0 ]; then
+ LOCK_COUNT=$(echo "$RG_LOCKS" | jq 'length')
+ LOCKS_FOUND=$((LOCKS_FOUND + LOCK_COUNT))
+
+ echo ""
+ echo "⚠️ Found $LOCK_COUNT RG-level lock(s) in: $RESOURCE_GROUP"
+ echo "$RG_LOCKS" | jq -r '.[] | " - \(.name) (\(.level))"'
+ echo " (RG-level locks also prevent child resource operations)"
+
+ # Remove RG-level locks
+ echo "$RG_LOCKS" | jq -r '.[].id' | while read LOCK_ID; do
+ if [ -n "$LOCK_ID" ]; then
+ LOCK_NAME=$(echo "$RG_LOCKS" | jq -r ".[] | select(.id==\"$LOCK_ID\") | .name")
+ echo " → Removing RG lock: $LOCK_NAME"
+ if az lock delete --ids "$LOCK_ID" 2>/dev/null; then
+ echo " ✓ Lock removed successfully"
+ LOCKS_REMOVED=$((LOCKS_REMOVED + 1))
+ else
+ echo " ⚠️ Failed to remove lock (may lack permissions)"
+ fi
+ fi
+ done
+ fi
+ fi
+ fi
+ fi
+ done
+
+ echo ""
+ echo "==================== Lock Check Complete ===================="
+ if [ $LOCKS_FOUND -gt 0 ]; then
+ echo "✓ Found and processed $LOCKS_FOUND lock(s)"
+ if [ $LOCKS_REMOVED -gt 0 ]; then
+ echo "✓ Successfully removed $LOCKS_REMOVED lock(s)"
+ fi
+ echo ""
+ echo "Terraform will now proceed with diagnostic settings changes."
+ else
+ echo "✓ No locks found on resources with diagnostic settings"
+ fi
+ echo ""
+ SCRIPT
+ }
+
+ # Also run on destroy to handle full cleanup
+ provisioner "local-exec" {
+ when = destroy
+ command = <<-SCRIPT
+ #!/bin/bash
+ set -e
+
+ echo "==================== Azure Lock Handler ===================="
+ echo "Checking for locks on resources with diagnostic settings being removed..."
+
+ # Get subscription ID from Azure CLI
+ SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null || echo "")
+
+ if [ -z "$SUBSCRIPTION_ID" ]; then
+ echo "⚠️ Warning: Unable to detect Azure subscription. Skipping lock check."
+ echo "Make sure you're logged in with: az login"
+ exit 0
+ fi
+
+ echo "✓ Detected subscription: $SUBSCRIPTION_ID"
+
+ # Get list of diagnostic settings being removed from terraform plan
+ echo ""
+ echo "Identifying resources with diagnostic settings being removed..."
+
+ # Get the list of diagnostic settings that will be destroyed
+ DIAG_SETTINGS=$(terraform state list 2>/dev/null | grep 'azurerm_monitor_diagnostic_setting.diagnostic_setting_logs' || echo "")
+
+ if [ -z "$DIAG_SETTINGS" ]; then
+ echo "✓ No diagnostic settings found in state - no locks to check"
+ exit 0
+ fi
+
+ LOCK_REMOVED=false
+
+ # For each diagnostic setting, extract the target resource ID and check for locks
+ echo "$DIAG_SETTINGS" | while read DIAG_SETTING_KEY; do
+ if [ -n "$DIAG_SETTING_KEY" ]; then
+ # Get the target resource ID from the diagnostic setting
+ TARGET_RESOURCE_ID=$(terraform state show "$DIAG_SETTING_KEY" 2>/dev/null | grep 'target_resource_id' | head -1 | awk '{print $3}' | tr -d '"' || echo "")
+
+ if [ -n "$TARGET_RESOURCE_ID" ]; then
+ echo ""
+ echo "Checking locks for resource: $TARGET_RESOURCE_ID"
+
+ # Extract resource type, name, and resource group from the ID
+ RESOURCE_TYPE=$(echo "$TARGET_RESOURCE_ID" | awk -F'/providers/' '{print $2}' | awk -F'/' '{print $1"/"$2}')
+ RESOURCE_NAME=$(echo "$TARGET_RESOURCE_ID" | awk -F'/' '{print $NF}')
+ RESOURCE_GROUP=$(echo "$TARGET_RESOURCE_ID" | awk -F'/resourceGroups/' '{print $2}' | awk -F'/' '{print $1}')
+
+ if [ -n "$RESOURCE_GROUP" ] && [ -n "$RESOURCE_NAME" ] && [ -n "$RESOURCE_TYPE" ]; then
+ # Check for locks on this specific resource
+ RESOURCE_LOCKS=$(az lock list \
+ --resource-group "$RESOURCE_GROUP" \
+ --resource-name "$RESOURCE_NAME" \
+ --resource-type "$RESOURCE_TYPE" \
+ --query "[].{name:name, id:id, level:level}" \
+ -o json 2>/dev/null || echo "[]")
+
+ if [ "$RESOURCE_LOCKS" != "[]" ] && [ -n "$RESOURCE_LOCKS" ]; then
+ echo " ⚠️ Found locks on this resource:"
+ echo "$RESOURCE_LOCKS" | jq -r '.[] | " - \(.name) (\(.level))"'
+
+ # Remove each lock
+ echo "$RESOURCE_LOCKS" | jq -r '.[].id' | while read LOCK_ID; do
+ if [ -n "$LOCK_ID" ]; then
+ LOCK_NAME=$(echo "$RESOURCE_LOCKS" | jq -r ".[] | select(.id==\"$LOCK_ID\") | .name")
+ echo " → Removing lock: $LOCK_NAME"
+ az lock delete --ids "$LOCK_ID" 2>/dev/null && LOCK_REMOVED=true || echo " ⚠️ Failed to remove lock (may lack permissions)"
+ fi
+ done
+ else
+ echo " ✓ No locks found on this resource"
+ fi
+
+ # Also check for locks at the resource group level
+ RG_LOCKS=$(az lock list \
+ --resource-group "$RESOURCE_GROUP" \
+ --query "[?contains(id, '/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Authorization/locks')].{name:name, id:id, level:level}" \
+ -o json 2>/dev/null || echo "[]")
+
+ if [ "$RG_LOCKS" != "[]" ] && [ -n "$RG_LOCKS" ]; then
+ echo " ⚠️ Found locks at resource group level ($RESOURCE_GROUP):"
+ echo "$RG_LOCKS" | jq -r '.[] | " - \(.name) (\(.level))"'
+ echo " → These RG-level locks may also prevent resource deletion"
+
+ # Remove RG-level locks
+ echo "$RG_LOCKS" | jq -r '.[].id' | while read LOCK_ID; do
+ if [ -n "$LOCK_ID" ]; then
+ LOCK_NAME=$(echo "$RG_LOCKS" | jq -r ".[] | select(.id==\"$LOCK_ID\") | .name")
+ echo " → Removing RG lock: $LOCK_NAME"
+ az lock delete --ids "$LOCK_ID" 2>/dev/null && LOCK_REMOVED=true || echo " ⚠️ Failed to remove lock (may lack permissions)"
+ fi
+ done
+ fi
+ fi
+ fi
+ fi
+ done
+
+ echo ""
+ echo "==================== Lock Check Complete ===================="
+ echo "✓ Lock removal process completed"
+ echo "If you see permission warnings above, you may need Owner or"
+ echo "User Access Administrator role to remove certain locks."
+ echo ""
+ SCRIPT
+ }
+}
diff --git a/azure-collection-terraform/output.tf b/azure-collection-terraform/output.tf
new file mode 100644
index 00000000..e0fe6710
--- /dev/null
+++ b/azure-collection-terraform/output.tf
@@ -0,0 +1,42 @@
+output "resource_group_name" {
+ value = azurerm_resource_group.rg.name
+}
+
+output "sumologic_collector_id" {
+ value = sumologic_collector.sumo_collector.id
+ description = "The ID of the Sumo Logic collector"
+}
+
+output "sumologic_activity_log_source_id" {
+ value = var.enable_activity_logs ? sumologic_azure_event_hub_log_source.sumo_activity_log_source[0].id : null
+ description = "The ID of the Sumo Logic activity log source"
+}
+
+output "sumologic_metrics_source_id" {
+ value = length(sumologic_azure_metrics_source.terraform_azure_metrics_source) > 0 ? values(sumologic_azure_metrics_source.terraform_azure_metrics_source)[0].id : null
+ description = "The ID of the first Sumo Logic metrics source"
+}
+
+output "eventhub_namespace_name" {
+ value = length(azurerm_eventhub_namespace.namespaces_by_location) > 0 ? values(azurerm_eventhub_namespace.namespaces_by_location)[0].name : null
+ description = "The name of the first Event Hub namespace"
+}
+
+output "eventhub_names" {
+ value = { for k, v in azurerm_eventhub.eventhubs_by_type_and_location : k => v.name }
+ description = "Map of Event Hub names by type and location"
+}
+
+output "sumologic_log_source_ids" {
+ value = { for k, v in sumologic_azure_event_hub_log_source.sumo_azure_event_hub_log_source : k => v.id }
+ description = "Map of Sumo Logic log source IDs by Event Hub"
+}
+
+output "installed_apps" {
+ value = { for k, v in sumologic_app.apps : k => {
+ uuid = v.uuid
+ name = k
+ id = v.id
+ } }
+ description = "Information about installed Sumo Logic apps"
+}
\ No newline at end of file
diff --git a/azure-collection-terraform/providers.tf b/azure-collection-terraform/providers.tf
new file mode 100644
index 00000000..c3ee286d
--- /dev/null
+++ b/azure-collection-terraform/providers.tf
@@ -0,0 +1,16 @@
+provider "sumologic" {
+ access_id = var.sumologic_access_id
+ access_key = var.sumologic_access_key
+ base_url = var.sumologic_environment_base_url
+ environment = var.sumologic_environment_base_url == null ? var.sumologic_environment : null
+}
+
+provider "azurerm" {
+ subscription_id = var.azure_subscription_id
+
+ features {
+ resource_group {
+ prevent_deletion_if_contains_resources = var.prevent_deletion_if_contains_resources
+ }
+ }
+}
\ No newline at end of file
diff --git a/azure-collection-terraform/sumologic_resources.tf b/azure-collection-terraform/sumologic_resources.tf
new file mode 100644
index 00000000..292a128f
--- /dev/null
+++ b/azure-collection-terraform/sumologic_resources.tf
@@ -0,0 +1,182 @@
+resource "sumologic_app" "apps" {
+ for_each = {
+ for app in var.installation_apps_list : app.name => app
+ }
+
+ uuid = each.value.uuid
+ version = each.value.version
+
+ parameters = each.value.parameters
+}
+
+resource "sumologic_collector" "sumo_collector" {
+ name = join("-", [var.sumo_collector_name, var.azure_subscription_id != null ? var.azure_subscription_id : data.azurerm_client_config.current.subscription_id])
+ description = "Azure Collector"
+ fields = {
+ tenant_name = "azure_account"
+ }
+}
+
+resource "sumologic_azure_event_hub_log_source" "sumo_azure_event_hub_log_source" {
+ for_each = {
+ for k, v in azurerm_eventhub.eventhubs_by_type_and_location : k => v
+ if length([
+ for config in var.target_resource_types :
+ config if config.log_namespace == local.eventhub_key_to_log_namespace[k] &&
+ config.log_namespace != null &&
+ config.log_namespace != ""
+ ]) > 0
+ }
+
+ name = each.value.name
+ description = "Azure Logs Source for ${each.key}"
+ category = "azure/logs/${each.key}"
+ content_type = "AzureEventHubLog"
+ collector_id = sumologic_collector.sumo_collector.id
+
+ fields = {
+ location = local.resources_by_type_and_location[each.key][0].location
+ }
+
+ authentication {
+ type = "AzureEventHubAuthentication"
+ shared_access_policy_name = azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy[
+ local.resources_by_type_and_location[each.key][0].location
+ ].name
+ shared_access_policy_key = azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy[
+ local.resources_by_type_and_location[each.key][0].location
+ ].primary_key
+ }
+
+ path {
+ type = "AzureEventHubPath"
+ namespace = azurerm_eventhub_namespace.namespaces_by_location[
+ local.resources_by_type_and_location[each.key][0].location
+ ].name
+ event_hub_name = each.value.name
+ consumer_group = "$Default"
+ region = "Commercial"
+ }
+
+ # Apply filters from target_resource_types configuration
+ dynamic "filters" {
+ for_each = flatten([
+ for config in var.target_resource_types :
+ [
+ for filter in config.log_source_filters :
+ filter
+ if config.log_namespace == local.eventhub_key_to_log_namespace[each.key] &&
+ config.log_namespace != null &&
+ config.log_namespace != "" &&
+ (length(filter.regions) == 0 || contains([for r in filter.regions : lower(replace(r, " ", ""))], lower(replace(local.resources_by_type_and_location[each.key][0].location, " ", ""))))
+ ]
+ if config.log_namespace == local.eventhub_key_to_log_namespace[each.key] &&
+ config.log_namespace != null &&
+ config.log_namespace != "" &&
+ length(config.log_source_filters) > 0
+ ])
+ content {
+ filter_type = filters.value.filter_type
+ name = filters.value.name
+ regexp = filters.value.regexp
+ mask = filters.value.mask
+ }
+ }
+}
+
+resource "sumologic_azure_metrics_source" "terraform_azure_metrics_source" {
+ for_each = {
+ for k, v in local.metrics_source_groups : k => v
+ if v.enabled == true && length(flatten(v.regions)) > 0
+ }
+
+ name = replace(replace(each.key, "/", "-"), ".", "-")
+ description = "Metrics for ${each.key}"
+ category = "azure/${lower(replace(replace(each.key, "/", "-"), ".", "-"))}/metrics"
+ content_type = "AzureMetrics"
+ scan_interval = var.scan_interval
+ collector_id = sumologic_collector.sumo_collector.id
+
+ authentication {
+ type = "AzureClientSecretAuthentication"
+ tenant_id = var.azure_tenant_id != null ? var.azure_tenant_id : data.azurerm_client_config.current.tenant_id
+ client_id = var.azure_client_id != null ? var.azure_client_id : data.azurerm_client_config.current.client_id
+ client_secret = var.azure_client_secret != null ? var.azure_client_secret : ""
+ }
+
+ path {
+ type = "AzureMetricsPath"
+ environment = "Azure"
+ limit_to_regions = flatten([for region in each.value.regions : [for r in region : lower(r)]])
+ limit_to_namespaces = each.value.namespaces
+
+ dynamic "azure_tag_filters" {
+ for_each = each.value.tag_filters
+ content {
+ type = azure_tag_filters.value.type
+ namespace = azure_tag_filters.value.namespace
+
+ dynamic "tags" {
+ for_each = azure_tag_filters.value.tags
+ content {
+ name = tags.value.name
+ values = tags.value.values
+ }
+ }
+ }
+ }
+ }
+
+ # Apply filters from target_resource_types configuration
+ dynamic "filters" {
+ for_each = flatten([
+ for config in var.target_resource_types :
+ config.metrics_source_filters
+ if config.metric_namespace == each.key &&
+ config.metric_namespace != null &&
+ config.metric_namespace != "" &&
+ length(config.metrics_source_filters) > 0
+ ])
+ content {
+ filter_type = filters.value.filter_type
+ name = filters.value.name
+ regexp = filters.value.regexp
+ mask = filters.value.mask
+ }
+ }
+}
+
+resource "sumologic_azure_event_hub_log_source" "sumo_activity_log_source" {
+ count = var.enable_activity_logs ? 1 : 0
+
+ name = var.activity_log_export_name
+ description = "Azure Subscription Activity Logs"
+ category = var.activity_log_export_category
+ content_type = "AzureEventHubLog"
+ collector_id = sumologic_collector.sumo_collector.id
+
+ authentication {
+ type = "AzureEventHubAuthentication"
+ shared_access_policy_name = azurerm_eventhub_namespace_authorization_rule.activity_logs_policy[0].name
+ shared_access_policy_key = azurerm_eventhub_namespace_authorization_rule.activity_logs_policy[0].primary_key
+ }
+
+ path {
+ type = "AzureEventHubPath"
+ namespace = azurerm_eventhub_namespace.activity_logs_namespace[0].name
+ event_hub_name = azurerm_eventhub.eventhub_for_activity_logs[0].name
+ consumer_group = "$Default"
+ region = "Commercial"
+ }
+
+ # Apply filters for activity logs
+ dynamic "filters" {
+ for_each = var.activity_log_filters
+ content {
+ filter_type = filters.value.filter_type
+ name = filters.value.name
+ regexp = filters.value.regexp
+ mask = filters.value.mask
+ }
+ }
+}
\ No newline at end of file
diff --git a/azure-collection-terraform/terraform.tfvars.example b/azure-collection-terraform/terraform.tfvars.example
new file mode 100644
index 00000000..b94300a4
--- /dev/null
+++ b/azure-collection-terraform/terraform.tfvars.example
@@ -0,0 +1,682 @@
+# ============================================================================
+# Terraform Configuration File - Example Template
+# ============================================================================
+# IMPORTANT: Keep all variables in this file, even if you're not using them.
+# - Leave variables empty or use default values instead of deleting them
+# - This prevents configuration errors and maintains consistency
+# - Example: If not using activity logs, set enable_activity_logs = false
+# (don't delete the variable)
+#
+# For detailed descriptions of all variables, see the Configuration Variables
+# section in README.md
+# ============================================================================
+
+# Azure Authentication Configuration
+azure_subscription_id = "your-azure-subscription-id"
+azure_client_id = "your-azure-client-id"
+azure_client_secret = "your-azure-client-secret"
+azure_tenant_id = "your-azure-tenant-id"
+
+# Azure Resource Configuration
+resource_group_name = "sumologic-azure-collection-rg"
+eventhub_namespace_name = "sumologic-azure-collection-EventHub"
+policy_name = "SumoLogicAzureCollectionPolicy"
+
+# Event Hub SKU Configuration (Global Defaults)
+# These settings apply to all regions unless overridden in region_specific_eventhub_skus
+# Valid SKUs: Basic, Standard, Premium, or Dedicated
+#
+# IMPORTANT: Auto-Upgrade Behavior
+# - If a region has more than 10 Event Hub instances and the SKU is Basic or Standard,
+# the system will automatically upgrade to Premium SKU for that region
+# - This is necessary because Basic/Standard SKUs have a limit of 10 Event Hubs per namespace
+# - Premium SKU supports up to 100 Event Hubs per namespace
+# - You can override this behavior using region_specific_eventhub_skus
+eventhub_namespace_sku = "Standard"
+
+# Throughput units (applies to Standard and Premium SKUs only)
+# Valid values: 1, 2, 4, 8, or 16
+# Note: Basic SKU has fixed throughput; Dedicated SKU uses Capacity Units instead
+default_throughput_units = 4
+
+# Optional: Region-Specific Event Hub SKU Configuration
+# Use this to override the global SKU and throughput settings for specific regions
+# This override takes precedence over auto-upgrade logic
+# Useful when:
+# - Different regions have different data volumes
+# - Cost optimization requires different SKUs per region
+# - Testing different configurations before global rollout
+# - Compliance requires specific SKU tiers in certain regions
+# - You want to prevent auto-upgrade to Premium (manually set SKU per region)
+#
+# Example:
+# region_specific_eventhub_skus = {
+# "East US" = {
+# sku = "Premium"
+# throughput_units = 8
+# }
+# "West Europe" = {
+# sku = "Standard"
+# throughput_units = 2
+# }
+# "Central US" = {
+# sku = "Basic"
+# throughput_units = 1 # Ignored for Basic SKU (fixed throughput)
+# }
+# }
+region_specific_eventhub_skus = {}
+
+# Regions that only support Basic & Standard SKUs for Event Hub namespaces
+eventhub_namespace_limited_sku_locations = [
+ "West India",
+ "Mexico Central",
+]
+
+# Regions where Event Hub Namespace creation is not supported
+eventhub_namespace_unsupported_locations = [
+ "South Africa West",
+ "Australia Central 2",
+ "USGov Arizona",
+ "USGov Texas",
+ "USGov Virginia",
+ "Brazil Southeast",
+ "China East",
+ "China East 2",
+ "China East 3",
+ "China North",
+ "China North 2",
+ "China North 3",
+ "France South",
+ "Germany North",
+ "Norway West",
+ "Sweden South",
+ "Switzerland West",
+ "Taiwan North",
+ "UAE Central",
+]
+
+# Azure Metrics Source Scan Interval
+# Time interval in milliseconds for scanning new metrics data
+# Default: 300000 (5 minutes)
+# Minimum: 1000 (1 second)
+# Lower values provide more frequent metric updates but increase API calls
+scan_interval = 300000
+
+# Controls azurerm provider behavior for Resource Group deletion.
+# Default is true (safe). Set to false in test environments to allow cleanup of
+# resource groups that still contain nested resources (useful for integration tests).
+prevent_deletion_if_contains_resources = true
+
+# Activity Log Configuration
+activity_log_export_name = "SumoLogicAzureActivityLogExport"
+activity_log_export_category = "azure/activity-logs"
+location = "East US"
+enable_activity_logs = false
+
+# Activity Log Filters Configuration
+# Filters applied at the Sumo Logic activity log source level
+# Activity logs are subscription-level, so no region filtering is available
+#
+# Filter types: "Include" (only match), "Exclude" (reject match), "Mask" (redact match)
+# Each filter requires:
+# - filter_type: "Include", "Exclude", or "Mask"
+# - name: Descriptive name for the filter
+# - regexp: RE2 regex pattern (must match entire log message)
+# - mask (optional): Replacement string for Mask filters (cannot contain colons)
+#
+# Filters are evaluated in order; first match wins
+#
+# Example configurations:
+# activity_log_filters = [
+# {
+# filter_type = "Include"
+# name = "Include Write operations only"
+# regexp = ".*\"operationName\".*write.*"
+# },
+# {
+# filter_type = "Mask"
+# name = "Mask sensitive fields"
+# regexp = "(\\\"password\\\".*)"
+# mask = "***REDACTED***" # Cannot contain colons
+# },
+# {
+# filter_type = "Exclude"
+# name = "Exclude read operations"
+# regexp = ".*\"operationName\".*read.*"
+# },
+# ]
+activity_log_filters = []
+
+# Resource Targeting Configuration
+# Complete list of all supported Azure resources (as of 17-Oct-2025)
+#
+# Each resource type can optionally specify:
+#
+# 1. log_categories - Filters which log categories to collect:
+# - Omit field: Collects ALL available log categories (default)
+# - Empty array []: Collects ALL available log categories (same as omitted)
+# - Specific categories: Only collects the specified log categories
+#
+# 2. required_resource_tags - Filters resources by tags (AND logic - all tags must match):
+# - Omit field: No tag filtering, monitors all resources of this type
+# - Empty map {}: No tag filtering (same as omitted)
+# - Specific tags: Only monitors resources with ALL specified tags
+#
+# 3. name_filter - Filters resources by regex pattern (case-insensitive):
+# - Omit field: No name filtering, monitors all resources of this type
+# - Empty string "": No name filtering (same as omitted)
+# - Regex pattern: Only monitors resources whose names match the pattern
+# - Examples: "^prod-.*" (starts with prod-), ".*-test$" (ends with -test),
+# "dev-.*-[0-9]+" (complex pattern), "^(prod|staging)-.*" (multiple prefixes)
+#
+# 4. log_source_filters - Filters applied at the Sumo Logic log source level:
+# - Omit field or empty array []: No content filtering at source level
+# - Filter types: "Include" (only match), "Exclude" (reject match), "Mask" (redact match)
+# - Each filter requires:
+# * filter_type: "Include", "Exclude", or "Mask"
+# * name: Descriptive name for the filter
+# * regexp: RE2 regex pattern (must match entire log message)
+# * mask (optional): Replacement string for Mask filters (cannot contain colons)
+# * regions (optional): List of regions to apply filter to (empty = all regions)
+# - Region matching is case-insensitive with spaces removed ("East US" = "eastus")
+# - Filters are evaluated in order; first match wins
+#
+# To find valid log categories for a resource, use Azure CLI:
+# az monitor diagnostic-settings categories list --resource
+#
+# Example with all optional filters:
+# {
+# log_namespace = "Microsoft.KeyVault/vaults"
+# metric_namespace = "Microsoft.KeyVault/vaults"
+# log_categories = ["AuditEvent", "AzurePolicyEvaluationDetails"]
+# required_resource_tags = { "environment" = "production", "team" = "security" }
+# name_filter = "^prod-.*"
+# log_source_filters = [
+# {
+# filter_type = "Include"
+# name = "Include only successful operations"
+# regexp = ".*\"ResultType\":\"Success\".*"
+# regions = ["East US", "West US"] # Apply only to these regions
+# },
+# {
+# filter_type = "Mask"
+# name = "Mask sensitive data"
+# regexp = "(\\\"password\\\"\\s*:\\s*\\\"[^\\\"]+\\\")"
+# mask = "***REDACTED***" # Cannot contain colons
+# # No regions specified = applies to all regions
+# },
+# {
+# filter_type = "Exclude"
+# name = "Exclude health checks"
+# regexp = ".*healthcheck.*"
+# },
+# ]
+# }
+target_resource_types = [
+ # Azure Application Gateway
+ {
+ log_namespace = "Microsoft.Network/applicationgateways"
+ metric_namespace = "Microsoft.Network/applicationgateways"
+
+ # Optional: Specify which log categories to collect
+ # - Omit this field or use [] to collect ALL available log categories
+ # - Specify categories to collect only those logs
+ # - Find valid categories: az monitor diagnostic-settings categories list --resource
+ # log_categories = ["ApplicationGatewayAccessLog", "ApplicationGatewayPerformanceLog", "ApplicationGatewayFirewallLog"]
+
+ # Optional: Filter resources by tags (AND logic - all tags must match)
+ # - Omit this field or use {} to monitor all resources of this type
+ # required_resource_tags = { "environment" = "production", "team" = "platform" }
+
+ # Optional: Filter resources by regex pattern (case-insensitive)
+ # - Omit this field or use "" to monitor all resources of this type
+ # - Examples: "^prod-.*" (starts with prod-), ".*-test$" (ends with -test)
+ # name_filter = "^prod-.*"
+ },
+ # Azure Load Balancer
+ {
+ log_namespace = "Microsoft.Network/loadBalancers"
+ metric_namespace = "Microsoft.Network/loadBalancers"
+ },
+ # Azure Cache for Redis
+ {
+ log_namespace = "Microsoft.Cache/redis"
+ metric_namespace = "Microsoft.Cache/redis"
+ },
+ # Azure Cache for Redis Enterprise
+ {
+ metric_namespace = "Microsoft.Cache/redisEnterprise"
+ },
+ # Azure Functions
+ {
+ log_namespace = "Microsoft.Web/sites"
+ metric_namespace = "Microsoft.Web/sites"
+ },
+ # Azure App Service Plan (metrics only)
+ {
+ metric_namespace = "Microsoft.Web/serverfarms"
+ },
+ # Azure Database for MySQL
+ {
+ log_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ metric_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ },
+ # Azure Cosmos DB (for NoSQL)
+ {
+ log_namespace = "Microsoft.DocumentDb/databaseAccounts"
+ metric_namespace = "Microsoft.DocumentDB/DatabaseAccounts"
+ },
+ # Azure Database for PostgreSQL
+ {
+ log_namespace = "Microsoft.DBforPostgreSQL/flexibleServers"
+ metric_namespace = "Microsoft.DBforPostgreSQL/flexibleServers"
+
+ # Optional: Collect only specific log categories (otherwise collects all)
+ # log_categories = ["PostgreSQLLogs", "PostgreSQLFlexSessions", "PostgreSQLFlexTableStats", "PostgreSQLFlexQueryStoreRuntime", "PostgreSQLFlexQueryStoreWaitStats"]
+ },
+ # Azure Cosmos DB for PostgreSQL
+ {
+ log_namespace = "Microsoft.DBforPostgreSQL/serverGroupsv2"
+ metric_namespace = "Microsoft.DBForPostgreSQL/serverGroupsv2"
+ },
+ # Azure API Management
+ {
+ log_namespace = "Microsoft.ApiManagement/service"
+ metric_namespace = "Microsoft.ApiManagement/service"
+ },
+ # Azure Service Bus
+ {
+ log_namespace = "Microsoft.ServiceBus/namespaces"
+ metric_namespace = "Microsoft.ServiceBus/Namespaces"
+
+ # Example: Apply log source filters (content filtering at Sumo Logic)
+ # log_source_filters = [
+ # {
+ # filter_type = "Include"
+ # name = "Include specific resources"
+ # regexp = ".*\"resourceId\".*SBUS002.*"
+ # regions = ["East US"] # Apply only to East US region
+ # },
+ # {
+ # filter_type = "Mask"
+ # name = "Mask credit cards"
+ # regexp = "(\\d{16})"
+ # mask = "MaskedID"
+ # # No regions specified = applies to all regions globally
+ # },
+ # ]
+ },
+ # Azure Event Grid - Domains
+ {
+ log_namespace = "Microsoft.EventGrid/domains"
+ metric_namespace = "Microsoft.EventGrid/domains"
+ },
+ # Azure Event Grid - System Topics
+ {
+ log_namespace = "Microsoft.EventGrid/systemTopics"
+ metric_namespace = "Microsoft.EventGrid/systemTopics"
+ },
+ # Azure Event Grid - Topics
+ {
+ log_namespace = "Microsoft.EventGrid/topics"
+ metric_namespace = "Microsoft.EventGrid/topics"
+ },
+ # Azure Event Grid - Namespaces
+ {
+ log_namespace = "Microsoft.EventGrid/namespaces"
+ metric_namespace = "Microsoft.EventGrid/namespaces"
+ },
+ # Azure Virtual Network
+ {
+ log_namespace = "Microsoft.Network/virtualNetworks"
+ metric_namespace = "Microsoft.Network/virtualNetworks"
+ },
+ # Azure Network Interfaces
+ {
+ log_namespace = "Microsoft.Network/networkInterfaces"
+ metric_namespace = "Microsoft.Network/networkInterfaces"
+ },
+ # Azure Container Instances (metrics only)
+ {
+ metric_namespace = "Microsoft.ContainerInstance/containerGroups"
+ },
+ # Azure Kubernetes Service (AKS) - Control Plane
+ {
+ log_namespace = "Microsoft.ContainerService/managedClusters"
+ metric_namespace = "Microsoft.ContainerService/managedClusters"
+ },
+ # Azure Virtual Machine (metrics only)
+ {
+ metric_namespace = "Microsoft.Compute/virtualMachines"
+ },
+ {
+ metric_namespace = "Microsoft.Compute/virtualmachineScaleSets"
+ },
+ # Azure App Service Environment (logs only)
+ {
+ log_namespace = "Microsoft.Web/hostingEnvironments"
+ },
+ # Azure Key Vaults
+ {
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ # Example: Collect specific log categories only
+ # log_categories = ["AuditEvent", "AzurePolicyEvaluationDetails"]
+
+ # Example: Apply all three optional filters together
+ # log_categories = ["AuditEvent"]
+ # required_resource_tags = { "environment" = "production", "team" = "security" }
+ # name_filter = "^prod-.*" # Only production key vaults starting with "prod-"
+ },
+ # Azure Storage (primary namespace - use nested_namespace_configs for nested resources)
+ {
+ log_namespace = "Microsoft.Storage/storageAccounts"
+ metric_namespace = "Microsoft.Storage/storageAccounts"
+ # Example: Collect all available log categories (default behavior)
+ # log_categories = []
+
+ # Example: Different name_filter patterns
+ # name_filter = "^prod-.*" # Starts with "prod-"
+ # name_filter = ".*-test$" # Ends with "-test"
+ # name_filter = ".*critical.*" # Contains "critical"
+ # name_filter = "^(prod|staging)-.*" # Starts with "prod-" or "staging-"
+ },
+ # Azure SQL
+ {
+ log_namespace = "Microsoft.Sql/servers/databases"
+ metric_namespace = "Microsoft.Sql/servers/databases"
+ },
+ {
+ log_namespace = "Microsoft.Sql/managedInstances"
+ metric_namespace = "Microsoft.Sql/managedInstances"
+ },
+ {
+ log_namespace = "Microsoft.Sql/servers/elasticpools"
+ metric_namespace = "Microsoft.Sql/servers/elasticpools"
+ },
+ # Azure Event Hubs - Namespaces
+ {
+ log_namespace = "Microsoft.EventHub/namespaces"
+ metric_namespace = "Microsoft.EventHub/Namespaces"
+ },
+ # Azure Event Hubs - Clusters (metrics only)
+ {
+ metric_namespace = "Microsoft.EventHub/clusters"
+ },
+ # Azure Machine Learning - Workspaces
+ {
+ log_namespace = "Microsoft.MachineLearningServices/workspaces"
+ metric_namespace = "Microsoft.MachineLearningServices/workspaces"
+ },
+ # Azure Machine Learning - Online Endpoints (metrics only)
+ {
+ metric_namespace = "Microsoft.MachineLearningServices/workspaces/onlineEndpoints"
+ },
+ # Azure Machine Learning - Online Endpoints Deployments (metrics only)
+ {
+ metric_namespace = "Microsoft.MachineLearningServices/workspaces/onlineEndpoints/deployments"
+ },
+ # Azure OpenAI
+ {
+ log_namespace = "Microsoft.CognitiveServices/accounts"
+ metric_namespace = "Microsoft.CognitiveServices/accounts"
+ }
+]
+
+required_resource_tags = {}
+
+# Nested namespace configurations for Azure Storage
+nested_namespace_configs = {
+ "Microsoft.Storage/storageAccounts" = [
+ "Microsoft.Storage/storageAccounts/blobServices",
+ "Microsoft.Storage/storageAccounts/fileServices",
+ "Microsoft.Storage/storageAccounts/queueServices",
+ "Microsoft.Storage/storageAccounts/tableServices"
+ ]
+}
+
+# Sumo Logic Configuration
+sumologic_access_id = "your-sumologic-access-id"
+sumologic_access_key = "your-sumologic-access-key"
+sumologic_environment = "us1"
+
+# Optional: Base URL for custom Sumo Logic environments
+# Use this for non-standard deployments like Switzerland (ch)
+# If provided, this takes precedence over sumologic_environment
+# Example: "https://api.ch.sumologic.com/api/"
+# Leave commented out for standard deployments (us1, us2, eu, au, etc.)
+# sumologic_environment_base_url = null
+
+sumo_collector_name = "sumologic-azure-collection"
+
+# App Installation Configuration
+# Complete list of all available Azure apps (as of 17-Oct-2025)
+#
+# Parameters explanation:
+# - All apps require "index_value" parameter (typically "sumologic_default")
+# - Some apps may support additional custom parameters
+# - Example with multiple parameters:
+# parameters = {
+# "index_value" = "sumologic_default"
+# "custom_param_1" = "value1"
+# "custom_param_2" = "value2"
+# }
+installation_apps_list = [
+ # Azure Application Gateway
+ {
+ uuid = "547aa2ec-1a6f-4fbe-84ca-e5e2bc57fd44"
+ name = "Azure Application Gateway"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ # Add additional app-specific parameters here if needed
+ }
+ },
+ # Azure Load Balancer
+ {
+ uuid = "63e4bd6d-8e15-41c4-a65d-74589574adf2"
+ name = "Azure Load Balancer"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Cache for Redis
+ {
+ uuid = "4a240798-d68f-464d-a6e8-ba2be85eba0f"
+ name = "Azure Cache for Redis"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Functions
+ {
+ uuid = "a0fb1bf0-2ab4-4f69-bf7e-5d97a176c7ea"
+ name = "Azure Functions"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Web Apps
+ {
+ uuid = "a4741497-31c6-4fb2-a236-0223e98b59e8"
+ name = "Azure Web Apps"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure App Service Plan
+ {
+ uuid = "cff0d2eb-9340-4108-8314-9faf9906f731"
+ name = "Azure App Service Plan"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Database for MySQL
+ {
+ uuid = "8bb63bc0-d6f4-4276-b546-780840c4d423"
+ name = "Azure Database for MySQL"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Cosmos DB (for NoSQL)
+ {
+ uuid = "d9ac4e28-13d6-4e69-8dcc-63fd6cb3bc80"
+ name = "Azure Cosmos DB (for NoSQL)"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Database for PostgreSQL
+ {
+ uuid = "73a729f2-49b1-4cf1-8494-04a605152358"
+ name = "Azure Database for PostgreSQL"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Cosmos DB for PostgreSQL
+ {
+ uuid = "aa97bc6c-7143-4cd9-9373-5f2ab456b4de"
+ name = "Azure Cosmos DB for PostgreSQL"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure API Management
+ {
+ uuid = "e78d9390-7365-4fdc-b309-e4a4b369b53d"
+ name = "Azure API Management"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Service Bus
+ {
+ uuid = "4bacb88a-0e8d-4c52-bea9-9f9656087459"
+ name = "Azure Service Bus"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Event Grid
+ {
+ uuid = "4e5b7c3f-b881-4dab-8609-5261ad14e420"
+ name = "Azure Event Grid"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Virtual Network
+ {
+ uuid = "37e3ed67-5f42-4acf-baf1-1dc44834039b"
+ name = "Azure Virtual Network"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Container Instances (ACI)
+ {
+ uuid = "bc9969aa-fb5b-49b7-96fb-d3f027396602"
+ name = "Azure Container Instances (ACI)"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Kubernetes Service (AKS) - Control Plane
+ {
+ uuid = "b387300a-3c29-4fb1-96b9-f67badc5d403"
+ name = "Azure Kubernetes Service (AKS) - Control Plane"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Virtual Machine
+ {
+ uuid = "dfa576fc-7d3b-4946-b414-149567e25d6a"
+ name = "Azure Virtual Machine"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure App Service Environment
+ {
+ uuid = "38c941ee-db70-484c-b14b-18e896240ff6"
+ name = "Azure App Service Environment"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Key Vaults
+ {
+ uuid = "449c796e-5da2-47ea-a304-e9299dd7435d"
+ name = "Azure Key Vaults"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Storage
+ {
+ uuid = "53376d23-2687-4500-b61e-4a2e2a119658"
+ name = "Azure Storage"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure SQL
+ {
+ uuid = "e6a61074-f173-458e-8c47-2e48b4b630a4"
+ name = "Azure SQL"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Event Hubs
+ {
+ uuid = "e6f1a9ec-eb5c-45ab-9970-57729b091f54"
+ name = "Azure Event Hubs"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure Machine Learning
+ {
+ uuid = "9efee397-c633-40eb-b7e5-3cc3bfb26ed1"
+ name = "Azure Machine Learning"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ # Azure OpenAI
+ {
+ uuid = "eca85221-e57a-4b33-958a-309ddd19c8ba"
+ name = "Azure OpenAI"
+ version = "latest"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ }
+]
\ No newline at end of file
diff --git a/azure-collection-terraform/test/azure_test.go b/azure-collection-terraform/test/azure_test.go
new file mode 100644
index 00000000..f6f9e35c
--- /dev/null
+++ b/azure-collection-terraform/test/azure_test.go
@@ -0,0 +1,872 @@
+package test
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/gruntwork-io/terratest/modules/terraform"
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ baseTfvarsFile = "test.tfvars"
+ fixturesDir = "fixtures"
+ terraformDir = "../"
+)
+
+// Helper function to create terraform options with base + override pattern
+func createTerraformOptions(tfvarsFile string) *terraform.Options {
+ var varFiles []string
+
+ // Always load base test.tfvars first for common configuration
+ baseVarFile := filepath.Join("test", baseTfvarsFile)
+ varFiles = append(varFiles, baseVarFile)
+
+ // If using a fixture file, load it second to override specific values
+ if tfvarsFile != baseTfvarsFile && tfvarsFile != "" {
+ // The tfvarsFile parameter already includes the full path (e.g., "test/fixtures/file.tfvars")
+ varFiles = append(varFiles, tfvarsFile)
+ }
+
+ return &terraform.Options{
+ TerraformDir: terraformDir,
+ VarFiles: varFiles,
+ NoColor: true,
+ }
+}
+
+// Helper function to run validation test (expects error)
+func runValidationTest(t *testing.T, testName string, tfvarsFile string, expectError bool, description string) {
+ terraformOptions := createTerraformOptions(tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ if expectError {
+ assert.Error(t, err, description)
+ // Verify it's a validation error, not an API error
+ if err != nil {
+ errStr := err.Error()
+ assert.True(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") ||
+ strings.Contains(errStr, "error_message"),
+ "Should be a validation error, got: %v", err)
+ }
+ } else {
+ // For valid configurations, we might get API errors trying to access resources
+ // We only care that validation passed
+ if err != nil {
+ errStr := err.Error()
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", testName, err)
+ } else {
+ // API errors are expected for validation-only tests
+ t.Logf("Test case '%s' passed validation but failed at runtime (expected): %v", testName, err)
+ }
+ }
+ }
+}
+
+func TestAzureSubscriptionIDValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidSubscriptionID",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-config.tfvars"),
+ expectError: false,
+ description: "Valid Azure subscription ID should pass validation",
+ },
+ {
+ name: "InvalidSubscriptionIDFormat",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-subscription.tfvars"),
+ expectError: true,
+ description: "Invalid subscription ID format should fail validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ })
+ }
+}
+
+func TestEventHubNamespaceNameValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidNamespaceName",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-config.tfvars"),
+ expectError: false,
+ description: "Valid Event Hub namespace name should pass validation",
+ },
+ {
+ name: "NamespaceNameTooShort",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-namespace.tfvars"),
+ expectError: true,
+ description: "Namespace name shorter than 6 characters should fail validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ })
+ }
+}
+
+func TestThroughputUnitsValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidThroughputUnits",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-config.tfvars"),
+ expectError: false,
+ description: "Valid throughput units should pass validation",
+ },
+ {
+ name: "MinimumThroughputUnits",
+ tfvarsFile: filepath.Join("test", fixturesDir, "min-throughput.tfvars"),
+ expectError: false,
+ description: "Minimum throughput units (1) should pass validation for both Standard and Premium",
+ },
+ {
+ name: "MaximumThroughputUnits",
+ tfvarsFile: filepath.Join("test", fixturesDir, "max-throughput.tfvars"),
+ expectError: false,
+ description: "Maximum throughput units (16) should pass validation for both Standard and Premium",
+ },
+ {
+ name: "ThroughputUnitsBelowMinimum",
+ tfvarsFile: filepath.Join("test", fixturesDir, "below-min-throughput.tfvars"),
+ expectError: true,
+ description: "Throughput units below minimum (0) should fail validation",
+ },
+ {
+ name: "ThroughputUnitsAboveMaximum",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-throughput.tfvars"),
+ expectError: true,
+ description: "Throughput units above maximum (25) should fail validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ })
+ }
+}
+
+func TestAzureResourceTypeFormatValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidResourceTypes",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-config.tfvars"),
+ expectError: false,
+ description: "Valid resource type formats should pass validation",
+ },
+ {
+ name: "InvalidResourceTypeFormats",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-resource-types.tfvars"),
+ expectError: true,
+ description: "Invalid resource type formats should fail validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ })
+ }
+}
+
+func TestEventHubNamespaceAuthorizationRulePermissions(t *testing.T) {
+ // Test that we can validate the authorization rule configuration exists
+ terraformOptions := createTerraformOptions(filepath.Join("test", fixturesDir, "valid-config.tfvars"))
+
+ terraform.Init(t, terraformOptions)
+
+ plan, err := terraform.PlanE(t, terraformOptions)
+
+ // We expect this might fail with API errors, but should not fail with validation errors
+ if err != nil {
+ errStr := err.Error()
+ assert.False(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule"),
+ "Should not have validation errors: %v", err)
+
+ // Log that this is expected for validation-only testing
+ t.Logf("Authorization rule test passed validation but failed at runtime (expected): %v", err)
+ return
+ }
+
+ // If plan succeeds, verify authorization rule exists with correct permissions
+ assert.Contains(t, plan, "azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy",
+ "Plan should contain Event Hub namespace authorization rule resource")
+
+ // Verify expected permissions in the plan
+ // Note: When the resource is being replaced, permissions may be shown as "unchanged attributes hidden"
+ // We check for explicit permissions OR the presence of unchanged attributes (which means permissions are preserved)
+ hasExplicitPermissions := strings.Contains(plan, "listen = true") &&
+ strings.Contains(plan, "send = true") &&
+ strings.Contains(plan, "manage = false")
+
+ hasUnchangedAttributes := strings.Contains(plan, "azurerm_eventhub_namespace_authorization_rule.sumo_collection_policy") &&
+ strings.Contains(plan, "unchanged attributes hidden")
+
+ assert.True(t, hasExplicitPermissions || hasUnchangedAttributes,
+ "Authorization rule should have correct permissions (listen=true, send=true, manage=false) either explicitly shown or preserved as unchanged")
+
+ t.Logf("✓ Authorization rule permissions verified (explicit: %v, unchanged: %v)", hasExplicitPermissions, hasUnchangedAttributes)
+}
+
+func TestBasicTerraformConfiguration(t *testing.T) {
+ // Test that the basic configuration passes validation
+ terraformOptions := createTerraformOptions(baseTfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ // We expect this might fail with API errors for missing resources,
+ // but it should not fail with validation errors
+ if err != nil {
+ errStr := err.Error()
+ assert.False(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule"),
+ "Basic configuration should not cause validation errors: %v", err)
+
+ t.Logf("Basic configuration passed validation but failed at runtime (expected for test environment): %v", err)
+ }
+}
+
+// Test naming convention logic (no Terraform execution required)
+func TestEventHubNamespaceNamingConventions(t *testing.T) {
+ testCases := []struct {
+ name string
+ inputLocation string
+ expectedTransformation string
+ description string
+ }{
+ {
+ name: "SpacesInLocation",
+ inputLocation: "East US",
+ expectedTransformation: "eastus",
+ description: "Spaces should be removed from location names",
+ },
+ {
+ name: "MixedCaseLocation",
+ inputLocation: "West US 2",
+ expectedTransformation: "westus2",
+ description: "Mixed case locations should be lowercased with spaces removed",
+ },
+ {
+ name: "AlreadyLowerCase",
+ inputLocation: "westus2",
+ expectedTransformation: "westus2",
+ description: "Already lowercase locations should remain unchanged",
+ },
+ {
+ name: "MultipleSpaces",
+ inputLocation: "Central US",
+ expectedTransformation: "centralus",
+ description: "Multiple spaces should be removed from location names",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Apply the same transformation as in the terraform resource
+ transformedLocation := strings.ReplaceAll(strings.ToLower(tc.inputLocation), " ", "")
+
+ assert.Equal(t, tc.expectedTransformation, transformedLocation,
+ fmt.Sprintf("Location transformation should match expected: %s", tc.expectedTransformation))
+
+ // Test full namespace name construction
+ testNamespaceName := "SUMO-HUB"
+ expectedFullName := fmt.Sprintf("%s-%s", testNamespaceName, transformedLocation)
+
+ assert.Contains(t, expectedFullName, testNamespaceName,
+ "Final namespace name should contain original namespace name")
+ assert.Contains(t, expectedFullName, transformedLocation,
+ "Final namespace name should contain transformed location")
+ })
+ }
+}
+
+func TestEventHubNamingConventions(t *testing.T) {
+ testCases := []struct {
+ name string
+ inputResourceType string
+ inputLocation string
+ expectedEventHubName string
+ description string
+ }{
+ {
+ name: "ResourceTypeWithSlashes",
+ inputResourceType: "Microsoft.KeyVault/vaults",
+ inputLocation: "eastus",
+ expectedEventHubName: "eventhub-Microsoft.KeyVault-vaults-eastus",
+ description: "Forward slashes in resource type should be replaced with hyphens",
+ },
+ {
+ name: "ResourceTypeWithMultipleSlashes",
+ inputResourceType: "Microsoft.Storage/storageAccounts/blobServices",
+ inputLocation: "westus2",
+ expectedEventHubName: "eventhub-Microsoft.Storage-storageAccounts-blobServices-westus2",
+ description: "Multiple forward slashes should all be replaced with hyphens",
+ },
+ {
+ name: "ResourceTypeWithDots",
+ inputResourceType: "Microsoft.Compute/virtualMachines",
+ inputLocation: "centralus",
+ expectedEventHubName: "eventhub-Microsoft.Compute-virtualMachines-centralus",
+ description: "Dots in resource type should be preserved, only slashes replaced",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ eachKey := fmt.Sprintf("%s-%s", tc.inputResourceType, tc.inputLocation)
+ transformedName := fmt.Sprintf("eventhub-%s", strings.ReplaceAll(eachKey, "/", "-"))
+
+ assert.Equal(t, tc.expectedEventHubName, transformedName,
+ fmt.Sprintf("Event Hub name transformation should match expected: %s", tc.expectedEventHubName))
+
+ assert.Contains(t, transformedName, "eventhub-",
+ "Event Hub name should start with 'eventhub-'")
+ assert.NotContains(t, transformedName, "/",
+ "Event Hub name should not contain forward slashes")
+ })
+ }
+}
+
+// TestAzureCredentialsValidation tests both format validation and actual Azure API authentication
+func TestAzureCredentialsValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ testType string // "format" or "api"
+ }{
+ {
+ name: "ValidCredentials",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-config.tfvars"),
+ expectError: false,
+ description: "Valid Azure credentials should pass validation and authenticate successfully",
+ testType: "api",
+ },
+ {
+ name: "InvalidSubscriptionIDFormat",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-subscription.tfvars"),
+ expectError: true,
+ description: "Invalid Azure subscription ID format should fail validation",
+ testType: "format",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.testType == "format" {
+ // Test format validation - should fail at Terraform validation stage
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ } else if tt.testType == "api" {
+ // Test actual Azure API authentication - should succeed with valid credentials
+ runAzureAPITest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ }
+ })
+ }
+}
+
+// Helper function to test actual Azure API authentication
+func runAzureAPITest(t *testing.T, testName string, tfvarsFile string, expectError bool, description string) {
+ terraformOptions := createTerraformOptions(tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+
+ plan, err := terraform.PlanE(t, terraformOptions)
+
+ if expectError {
+ assert.Error(t, err, description)
+ } else {
+ // For valid credentials, we expect the plan to succeed
+ // Even if there are runtime errors, they should be Azure resource-related, not authentication-related
+ if err != nil {
+ errStr := err.Error()
+ // Ensure it's not a validation error
+ assert.False(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule"),
+ "Should not have validation errors with valid credentials: %v", err)
+
+ // Ensure it's not an authentication error (401 = auth failed, 403 = authorized but insufficient permissions)
+ assert.False(t,
+ strings.Contains(errStr, "authentication failed") ||
+ strings.Contains(errStr, "invalid credentials") ||
+ strings.Contains(errStr, "401"),
+ "Should not have authentication errors with valid credentials: %v", err)
+
+ // If there are errors, they should be about Azure resources or API limits, not credentials
+ if strings.Contains(errStr, "Terraform planned the following actions") {
+ t.Logf("✓ %s: Valid credentials successfully authenticated and generated Terraform plan", testName)
+ t.Logf("Runtime error is related to Azure resources, not authentication: %v", err)
+ } else {
+ t.Logf("Warning: %s had unexpected error type: %v", testName, err)
+ }
+ } else {
+ // Perfect - plan succeeded completely
+ assert.NotEmpty(t, plan, "Plan should not be empty for valid credentials")
+ t.Logf("✓ %s: Valid credentials successfully authenticated and completed plan", testName)
+ }
+ }
+}
+
+// TestAzureCredentialsFormatValidation tests Azure credential format validation - ENHANCED COVERAGE
+func TestAzureCredentialsFormatValidation(t *testing.T) {
+ testCases := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidAzureCredentialsFormat",
+ tfvarsFile: "test/fixtures/valid-config.tfvars",
+ expectError: false,
+ description: "Valid Azure credentials should pass validation",
+ },
+ {
+ name: "InvalidTenantIDFormat",
+ tfvarsFile: "test/fixtures/invalid-tenant-id.tfvars",
+ expectError: true,
+ description: "Invalid tenant ID format should fail validation",
+ },
+ {
+ name: "EmptyTenantID",
+ tfvarsFile: "test/fixtures/empty-tenant-id.tfvars",
+ expectError: true,
+ description: "Empty tenant ID should fail validation",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tc.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ if tc.expectError {
+ assert.Error(t, err, tc.description)
+ if err != nil {
+ errStr := err.Error()
+ isValidationError := strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") ||
+ strings.Contains(errStr, "error_message")
+
+ if isValidationError {
+ t.Logf("✓ %s correctly failed with validation error: %s", tc.name, tc.description)
+ } else {
+ t.Logf("⚠️ %s failed but not with validation error: %v", tc.name, err)
+ }
+ }
+ } else {
+ if err != nil {
+ errStr := err.Error()
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tc.name, err)
+ } else {
+ t.Logf("✓ %s passed validation but failed at runtime (expected): %s", tc.name, tc.description)
+ }
+ } else {
+ t.Logf("✓ %s passed validation completely: %s", tc.name, tc.description)
+ }
+ }
+ })
+ }
+}
+
+func TestResourceGroupNameValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidResourceGroupName",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-config.tfvars"),
+ expectError: false,
+ description: "Valid resource group name should pass validation",
+ },
+ {
+ name: "InvalidSpecialCharacters",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-resource-group-special-chars.tfvars"),
+ expectError: true,
+ description: "Resource group name with spaces and special characters (@) should fail validation",
+ },
+ {
+ name: "InvalidStartsWithHyphen",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-resource-group-starts-hyphen.tfvars"),
+ expectError: true,
+ description: "Resource group name starting with hyphen should fail validation",
+ },
+ {
+ name: "InvalidEndsWithPeriod",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-resource-group-ends-period.tfvars"),
+ expectError: true,
+ description: "Resource group name ending with period should fail validation",
+ },
+ {
+ name: "InvalidReservedName",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-resource-group-reserved-name.tfvars"),
+ expectError: true,
+ description: "Resource group name using reserved name 'azure' should fail validation",
+ },
+ {
+ name: "InvalidTooLong",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-resource-group-too-long.tfvars"),
+ expectError: true,
+ description: "Resource group name exceeding 90 characters should fail validation",
+ },
+ {
+ name: "InvalidEmpty",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-resource-group-empty.tfvars"),
+ expectError: true,
+ description: "Empty resource group name should fail validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ })
+ }
+}
+
+// TestEventHubNamespaceRegionHandling tests the region-specific logic for Event Hub namespaces
+func TestEventHubNamespaceRegionHandling(t *testing.T) {
+ testCases := []struct {
+ name string
+ location string
+ unsupportedLocations []string
+ limitedSKULocations []string
+ expectedSKU string
+ shouldSkipCreation bool
+ description string
+ }{
+ {
+ name: "SupportedRegionWithPremiumSKU",
+ location: "East US",
+ unsupportedLocations: []string{"China East", "USGov Arizona"},
+ limitedSKULocations: []string{"West India", "Mexico Central"},
+ expectedSKU: "Premium",
+ shouldSkipCreation: false,
+ description: "Supported regions should allow Premium SKU",
+ },
+ {
+ name: "UnsupportedRegionShouldSkip",
+ location: "China East",
+ unsupportedLocations: []string{"China East", "USGov Arizona"},
+ limitedSKULocations: []string{"West India", "Mexico Central"},
+ expectedSKU: "",
+ shouldSkipCreation: true,
+ description: "Unsupported regions should skip Event Hub namespace creation",
+ },
+ {
+ name: "LimitedSKURegionForcesStandard",
+ location: "West India",
+ unsupportedLocations: []string{"China East", "USGov Arizona"},
+ limitedSKULocations: []string{"West India", "Mexico Central"},
+ expectedSKU: "Standard",
+ shouldSkipCreation: false,
+ description: "Limited-SKU regions should force Standard SKU even if Premium is configured",
+ },
+ {
+ name: "CaseInsensitiveRegionMatching",
+ location: "west india",
+ unsupportedLocations: []string{"China East"},
+ limitedSKULocations: []string{"West India", "Mexico Central"},
+ expectedSKU: "Standard",
+ shouldSkipCreation: false,
+ description: "Region matching should be case-insensitive",
+ },
+ {
+ name: "SpaceIgnoredInRegionMatching",
+ location: "WestIndia",
+ unsupportedLocations: []string{"China East"},
+ limitedSKULocations: []string{"West India"},
+ expectedSKU: "Standard",
+ shouldSkipCreation: false,
+ description: "Region matching should ignore spaces",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Normalize the location for comparison (same logic as in locals.tf)
+ normalizedLocation := strings.ReplaceAll(strings.ToLower(tc.location), " ", "")
+
+ // Check if location should be skipped
+ shouldSkip := false
+ for _, unsupportedLoc := range tc.unsupportedLocations {
+ normalizedUnsupported := strings.ReplaceAll(strings.ToLower(unsupportedLoc), " ", "")
+ if normalizedLocation == normalizedUnsupported {
+ shouldSkip = true
+ break
+ }
+ }
+
+ assert.Equal(t, tc.shouldSkipCreation, shouldSkip,
+ fmt.Sprintf("Location %s skip status should match expected", tc.location))
+
+ if !shouldSkip {
+ // Check if location has limited SKU support
+ forcedToStandard := false
+ for _, limitedLoc := range tc.limitedSKULocations {
+ normalizedLimited := strings.ReplaceAll(strings.ToLower(limitedLoc), " ", "")
+ if normalizedLocation == normalizedLimited {
+ forcedToStandard = true
+ break
+ }
+ }
+
+ expectedSKU := "Premium" // Default assumption
+ if forcedToStandard {
+ expectedSKU = "Standard"
+ }
+
+ assert.Equal(t, tc.expectedSKU, expectedSKU,
+ fmt.Sprintf("Location %s should have expected SKU: %s", tc.location, tc.description))
+ }
+
+ t.Logf("✓ %s: %s", tc.name, tc.description)
+ })
+ }
+}
+
+// TestEventHubNamespaceThroughputByRegion tests throughput unit assignment based on region and SKU
+func TestEventHubNamespaceThroughputByRegion(t *testing.T) {
+ testCases := []struct {
+ name string
+ location string
+ limitedSKULocations []string
+ configuredSKU string
+ standardThroughput int
+ premiumThroughput int
+ expectedThroughput int
+ description string
+ }{
+ {
+ name: "PremiumSKUInSupportedRegion",
+ location: "East US",
+ limitedSKULocations: []string{"West India"},
+ configuredSKU: "Premium",
+ standardThroughput: 2,
+ premiumThroughput: 4,
+ expectedThroughput: 4,
+ description: "Premium SKU in supported region should use premium_throughput_units",
+ },
+ {
+ name: "StandardSKUInSupportedRegion",
+ location: "East US",
+ limitedSKULocations: []string{"West India"},
+ configuredSKU: "Standard",
+ standardThroughput: 2,
+ premiumThroughput: 4,
+ expectedThroughput: 2,
+ description: "Standard SKU should use standard_throughput_units",
+ },
+ {
+ name: "LimitedSKURegionUsesStandardThroughput",
+ location: "West India",
+ limitedSKULocations: []string{"West India", "Mexico Central"},
+ configuredSKU: "Premium",
+ standardThroughput: 2,
+ premiumThroughput: 4,
+ expectedThroughput: 2,
+ description: "Limited-SKU region should use standard_throughput_units regardless of configured SKU",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Simulate the logic from azure_resources.tf
+ normalizedLocation := strings.ReplaceAll(strings.ToLower(tc.location), " ", "")
+
+ // Check if location has limited SKU support
+ isLimitedSKU := false
+ for _, limitedLoc := range tc.limitedSKULocations {
+ normalizedLimited := strings.ReplaceAll(strings.ToLower(limitedLoc), " ", "")
+ if normalizedLocation == normalizedLimited {
+ isLimitedSKU = true
+ break
+ }
+ }
+
+ var actualThroughput int
+ if isLimitedSKU {
+ actualThroughput = tc.standardThroughput
+ } else if tc.configuredSKU == "Premium" {
+ actualThroughput = tc.premiumThroughput
+ } else {
+ actualThroughput = tc.standardThroughput
+ }
+
+ assert.Equal(t, tc.expectedThroughput, actualThroughput,
+ fmt.Sprintf("%s: Expected throughput %d but got %d", tc.description, tc.expectedThroughput, actualThroughput))
+
+ t.Logf("✓ %s: throughput=%d", tc.description, actualThroughput)
+ })
+ }
+}
+
+// TestAzureResourceTagFiltering tests per-resource-type required_resource_tags for log sources
+// Validates that tag filtering works correctly for Azure diagnostic settings and log collection
+func TestAzureResourceTagFiltering(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "NoRequiredResourceTags",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-omitted.tfvars"),
+ expectError: false,
+ description: "Omitted required_resource_tags field should discover all Azure resources for log collection",
+ },
+ {
+ name: "EmptyRequiredResourceTags",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-empty.tfvars"),
+ expectError: false,
+ description: "Empty required_resource_tags = {} should discover all Azure resources for log collection",
+ },
+ {
+ name: "SingleTagFilter",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-single.tfvars"),
+ expectError: false,
+ description: "Single tag in required_resource_tags should filter Azure resources for log collection",
+ },
+ {
+ name: "MultipleTagsFilter",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-multiple.tfvars"),
+ expectError: false,
+ description: "Multiple tags should use AND logic to filter Azure resources for log collection",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tt.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ if tt.expectError {
+ assert.Error(t, err, tt.description)
+ } else {
+ // For valid configurations, validation should pass
+ // We might get API errors if resources don't exist, but validation itself should pass
+ if err != nil {
+ errStr := err.Error()
+ // Check for validation errors - these would indicate a problem
+ if strings.Contains(errStr, "Invalid category") ||
+ strings.Contains(errStr, "Invalid log categories detected") ||
+ strings.Contains(errStr, "Resource precondition failed") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tt.name, err)
+ } else {
+ // API/runtime errors are expected for validation-only tests without real resources
+ t.Logf("✓ Test case '%s' passed validation but failed at runtime (expected): %v", tt.name, err)
+ }
+ } else {
+ t.Logf("✅ Test case '%s' passed successfully", tt.name)
+ }
+ }
+ })
+ }
+}
+
+// TestAzureResourceNameFiltering tests name_filter regex functionality for filtering Azure resources
+// Validates that name filtering works correctly for log collection
+func TestAzureResourceNameFiltering(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "OmittedNameFilter",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-omitted.tfvars"),
+ expectError: false,
+ description: "Omitted name_filter should discover all Azure resources matching tag filters",
+ },
+ {
+ name: "EmptyNameFilter",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-empty.tfvars"),
+ expectError: false,
+ description: "Empty name_filter = \"\" should discover all Azure resources matching tag filters",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tt.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+
+ plan, err := terraform.PlanE(t, terraformOptions)
+
+ if tt.expectError {
+ assert.Error(t, err, tt.description)
+ } else {
+ // For valid configurations, validation should pass
+ // Check the plan output to verify resources were filtered correctly
+ if err != nil {
+ errStr := err.Error()
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tt.name, err)
+ } else {
+ t.Logf("✓ Test case '%s' passed validation but failed at runtime (expected): %v", tt.name, err)
+ }
+ } else {
+ t.Logf("✅ Test case '%s' passed successfully", tt.name)
+ t.Logf("✓ Test case '%s': Plan includes Azure resource filtering", tt.name)
+
+ // Verify plan contains expected resources
+ assert.Contains(t, plan, "azurerm_resource_group",
+ "Plan should contain resource group")
+ }
+ }
+ })
+ }
+}
diff --git a/azure-collection-terraform/test/fixtures/activity-log-filters-invalid-mask.tfvars b/azure-collection-terraform/test/fixtures/activity-log-filters-invalid-mask.tfvars
new file mode 100644
index 00000000..ef06e7a5
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/activity-log-filters-invalid-mask.tfvars
@@ -0,0 +1,11 @@
+# Invalid filter - mask string with colons (should fail)
+enable_activity_logs = true
+
+activity_log_filters = [
+ {
+ filter_type = "Mask"
+ name = "Invalid mask with colons"
+ regexp = "(\\\"password\\\"\\s*:\\s*\\\"[^\\\"]+\\\")"
+ mask = "\"password\":\"REDACTED\"" # Contains colons - should fail
+ },
+]
diff --git a/azure-collection-terraform/test/fixtures/activity-log-filters-valid.tfvars b/azure-collection-terraform/test/fixtures/activity-log-filters-valid.tfvars
new file mode 100644
index 00000000..27181573
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/activity-log-filters-valid.tfvars
@@ -0,0 +1,16 @@
+# Activity log filters configuration
+enable_activity_logs = true
+
+activity_log_filters = [
+ {
+ filter_type = "Include"
+ name = "Include Write operations"
+ regexp = ".*\"operationName\"\\s*:\\s*\"[^\"]*write[^\"]*\".*"
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask sensitive data"
+ regexp = "(\\\"password\\\"\\s*:\\s*\\\"[^\\\"]+\\\")"
+ mask = "***REDACTED***"
+ },
+]
diff --git a/azure-collection-terraform/test/fixtures/activity-logs-disabled.tfvars b/azure-collection-terraform/test/fixtures/activity-logs-disabled.tfvars
new file mode 100644
index 00000000..6b3e202f
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/activity-logs-disabled.tfvars
@@ -0,0 +1,2 @@
+# Configuration with Activity Logs disabled
+enable_activity_logs = false
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/activity-logs-enabled.tfvars b/azure-collection-terraform/test/fixtures/activity-logs-enabled.tfvars
new file mode 100644
index 00000000..e027f466
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/activity-logs-enabled.tfvars
@@ -0,0 +1,2 @@
+# Configuration with Activity Logs enabled
+enable_activity_logs = true
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/below-min-throughput.tfvars b/azure-collection-terraform/test/fixtures/below-min-throughput.tfvars
new file mode 100644
index 00000000..7e626605
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/below-min-throughput.tfvars
@@ -0,0 +1,4 @@
+# Below minimum throughput units test (should fail)
+# Only overrides the specific parameter being tested - throughput units below minimum
+# All other values inherited from test.tfvars
+default_throughput_units = 0
diff --git a/azure-collection-terraform/test/fixtures/empty-log-categories.tfvars b/azure-collection-terraform/test/fixtures/empty-log-categories.tfvars
new file mode 100644
index 00000000..0a9355ed
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/empty-log-categories.tfvars
@@ -0,0 +1,37 @@
+# Empty log categories test fixture
+# Tests that empty log_categories array passes validation and enables all categories
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ log_categories = [] # Empty array - should enable all available categories
+}]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/empty-tenant-id.tfvars b/azure-collection-terraform/test/fixtures/empty-tenant-id.tfvars
new file mode 100644
index 00000000..0525ac30
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/empty-tenant-id.tfvars
@@ -0,0 +1,2 @@
+# Empty tenant ID string - should fail validation (empty string not allowed, but null is OK)
+azure_tenant_id = ""
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-log-category.tfvars b/azure-collection-terraform/test/fixtures/invalid-log-category.tfvars
new file mode 100644
index 00000000..d6802c42
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-log-category.tfvars
@@ -0,0 +1,37 @@
+# Invalid log category test fixture
+# Tests that invalid log categories fail validation
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ log_categories = ["InvalidCategory123"] # Invalid category - should fail validation
+}]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/invalid-namespace.tfvars b/azure-collection-terraform/test/fixtures/invalid-namespace.tfvars
new file mode 100644
index 00000000..03519af6
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-namespace.tfvars
@@ -0,0 +1,3 @@
+# Invalid namespace name (too long) for testing validation
+# Inherits real credentials from test.tfvars, only overrides the namespace name being tested
+eventhub_namespace_name = "this-namespace-name-is-way-too-long-for-azure-eventhub-namespace-maximum-length"
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-resource-group-empty.tfvars b/azure-collection-terraform/test/fixtures/invalid-resource-group-empty.tfvars
new file mode 100644
index 00000000..14381940
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-resource-group-empty.tfvars
@@ -0,0 +1,4 @@
+# Empty resource group name
+# Only overrides the specific parameter being tested - resource_group_name as empty string
+# All other values inherited from test.tfvars
+resource_group_name = "" # Invalid: empty string
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-resource-group-ends-period.tfvars b/azure-collection-terraform/test/fixtures/invalid-resource-group-ends-period.tfvars
new file mode 100644
index 00000000..1fceb39c
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-resource-group-ends-period.tfvars
@@ -0,0 +1,2 @@
+# Invalid resource group name that ends with period
+resource_group_name = "test-sumo-rg." # Invalid: ends with period
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-resource-group-reserved-name.tfvars b/azure-collection-terraform/test/fixtures/invalid-resource-group-reserved-name.tfvars
new file mode 100644
index 00000000..d5cdd056
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-resource-group-reserved-name.tfvars
@@ -0,0 +1,2 @@
+# Invalid resource group name using reserved name "azure"
+resource_group_name = "azure" # Invalid: reserved name
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-resource-group-special-chars.tfvars b/azure-collection-terraform/test/fixtures/invalid-resource-group-special-chars.tfvars
new file mode 100644
index 00000000..2df2557a
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-resource-group-special-chars.tfvars
@@ -0,0 +1,2 @@
+# Invalid resource group name with special characters (spaces and @)
+resource_group_name = "test sumo@rg" # Invalid: contains space and @
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-resource-group-starts-hyphen.tfvars b/azure-collection-terraform/test/fixtures/invalid-resource-group-starts-hyphen.tfvars
new file mode 100644
index 00000000..2ae794fe
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-resource-group-starts-hyphen.tfvars
@@ -0,0 +1,2 @@
+# Invalid resource group name that starts with hyphen
+resource_group_name = "-test-sumo-rg" # Invalid: starts with hyphen
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-resource-group-too-long.tfvars b/azure-collection-terraform/test/fixtures/invalid-resource-group-too-long.tfvars
new file mode 100644
index 00000000..cafa4dce
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-resource-group-too-long.tfvars
@@ -0,0 +1,2 @@
+# Invalid resource group name that's too long (over 90 characters)
+resource_group_name = "test-sumo-rg-with-a-very-long-name-that-exceeds-the-maximum-allowed-length-of-90-characters-for-azure-resource-groups" # Invalid: 120+ characters
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-resource-types.tfvars b/azure-collection-terraform/test/fixtures/invalid-resource-types.tfvars
new file mode 100644
index 00000000..063addff
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-resource-types.tfvars
@@ -0,0 +1,15 @@
+# Invalid resource type format test (should fail validation)
+target_resource_types = [
+ {
+ log_namespace = "InvalidFormat" # Missing required slash
+ metric_namespace = "InvalidFormat"
+ },
+ {
+ log_namespace = "NoSlash" # Missing required slash
+ metric_namespace = "NoSlash"
+ },
+ {
+ log_namespace = "Microsoft." # Ends with dot (invalid format)
+ metric_namespace = "Microsoft."
+ }
+]
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-subscription.tfvars b/azure-collection-terraform/test/fixtures/invalid-subscription.tfvars
new file mode 100644
index 00000000..52a26ada
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-subscription.tfvars
@@ -0,0 +1,4 @@
+# Invalid subscription ID format for testing validation
+# Only overrides the specific parameter being tested - azure_subscription_id
+# All other values inherited from test.tfvars
+azure_subscription_id = "invalid-subscription-id"
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-tenant-id.tfvars b/azure-collection-terraform/test/fixtures/invalid-tenant-id.tfvars
new file mode 100644
index 00000000..7975ce6e
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-tenant-id.tfvars
@@ -0,0 +1,2 @@
+# Invalid tenant ID format - should fail validation
+azure_tenant_id = "invalid-tenant-id-format"
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/invalid-throughput.tfvars b/azure-collection-terraform/test/fixtures/invalid-throughput.tfvars
new file mode 100644
index 00000000..0dfcba61
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/invalid-throughput.tfvars
@@ -0,0 +1,2 @@
+# Invalid throughput units (above maximum) for testing validation
+default_throughput_units = 25
diff --git a/azure-collection-terraform/test/fixtures/log-source-filters-valid.tfvars b/azure-collection-terraform/test/fixtures/log-source-filters-valid.tfvars
new file mode 100644
index 00000000..e37c8fdd
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/log-source-filters-valid.tfvars
@@ -0,0 +1,19 @@
+# Valid log source filters configuration
+target_resource_types = [{
+ log_namespace = "Microsoft.ServiceBus/namespaces"
+ metric_namespace = "Microsoft.ServiceBus/Namespaces"
+ log_source_filters = [
+ {
+ filter_type = "Include"
+ name = "Include specific resources"
+ regexp = ".*\"resourceId\"\\s*:\\s*\"[^\"]*test[^\"]*\".*"
+ regions = ["East US"]
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask credit cards"
+ regexp = "(\\d{16})"
+ mask = "XXXX-XXXX-XXXX-XXXX"
+ },
+ ]
+}]
diff --git a/azure-collection-terraform/test/fixtures/max-throughput.tfvars b/azure-collection-terraform/test/fixtures/max-throughput.tfvars
new file mode 100644
index 00000000..624ffa7b
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/max-throughput.tfvars
@@ -0,0 +1,4 @@
+# Maximum throughput units test (should pass)
+# Only overrides the specific parameter being tested - throughput units at maximum
+# All other values inherited from test.tfvars
+default_throughput_units = 16
diff --git a/azure-collection-terraform/test/fixtures/metrics-source-filters-invalid-mask.tfvars b/azure-collection-terraform/test/fixtures/metrics-source-filters-invalid-mask.tfvars
new file mode 100644
index 00000000..d96d1477
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/metrics-source-filters-invalid-mask.tfvars
@@ -0,0 +1,13 @@
+# Invalid metrics source filters - mask with colons
+# This should fail Sumo Logic API validation
+target_resource_types = [{
+ metric_namespace = "Microsoft.ServiceBus/Namespaces"
+ metrics_source_filters = [
+ {
+ filter_type = "Mask"
+ name = "Invalid mask with colons"
+ regexp = "(\\d{16})"
+ mask = "INVALID:MASK:VALUE" # Colons not allowed in mask strings
+ },
+ ]
+}]
diff --git a/azure-collection-terraform/test/fixtures/metrics-source-filters-multiple.tfvars b/azure-collection-terraform/test/fixtures/metrics-source-filters-multiple.tfvars
new file mode 100644
index 00000000..fa8bc57a
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/metrics-source-filters-multiple.tfvars
@@ -0,0 +1,22 @@
+# Multiple valid metrics source filters with different filter types
+target_resource_types = [{
+ metric_namespace = "Microsoft.ServiceBus/Namespaces"
+ metrics_source_filters = [
+ {
+ filter_type = "Include"
+ name = "Include production resources"
+ regexp = ".*\"resourceId\".*prod.*"
+ },
+ {
+ filter_type = "Exclude"
+ name = "Exclude test resources"
+ regexp = ".*\"resourceId\".*test.*"
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask subscription IDs"
+ regexp = "(\"subscriptionId\"\\s*:\\s*\"[^\"]+\")"
+ mask = "MASKED_SUBSCRIPTION"
+ },
+ ]
+}]
diff --git a/azure-collection-terraform/test/fixtures/metrics-source-filters-valid.tfvars b/azure-collection-terraform/test/fixtures/metrics-source-filters-valid.tfvars
new file mode 100644
index 00000000..c6887bce
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/metrics-source-filters-valid.tfvars
@@ -0,0 +1,17 @@
+# Valid metrics source filters configuration
+target_resource_types = [{
+ metric_namespace = "Microsoft.ServiceBus/Namespaces"
+ metrics_source_filters = [
+ {
+ filter_type = "Include"
+ name = "Include SBUS002 resources"
+ regexp = ".*\"resourceId\"\\s*:\\s*\"[^\"]*SBUS002[^\"]*\".*"
+ },
+ {
+ filter_type = "Mask"
+ name = "Mask sensitive IDs"
+ regexp = "(\\d{16})"
+ mask = "MASKED_ID"
+ },
+ ]
+}]
diff --git a/azure-collection-terraform/test/fixtures/min-throughput.tfvars b/azure-collection-terraform/test/fixtures/min-throughput.tfvars
new file mode 100644
index 00000000..e6d74c6e
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/min-throughput.tfvars
@@ -0,0 +1,4 @@
+# Minimum throughput units test (should pass)
+# Only overrides the specific parameter being tested - throughput units at minimum
+# All other values inherited from test.tfvars
+default_throughput_units = 1
diff --git a/azure-collection-terraform/test/fixtures/mixed-log-categories.tfvars b/azure-collection-terraform/test/fixtures/mixed-log-categories.tfvars
new file mode 100644
index 00000000..a1f1b364
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/mixed-log-categories.tfvars
@@ -0,0 +1,37 @@
+# Mixed valid and invalid log categories test fixture
+# Tests that mix of valid and invalid categories fails validation
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ log_categories = ["AuditEvent", "InvalidCategory", "AzurePolicyEvaluationDetails"] # Mix: 2 valid, 1 invalid
+}]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/multiple-resources-mixed-categories.tfvars b/azure-collection-terraform/test/fixtures/multiple-resources-mixed-categories.tfvars
new file mode 100644
index 00000000..b0346497
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/multiple-resources-mixed-categories.tfvars
@@ -0,0 +1,49 @@
+# Multiple resource types with mixed category configurations test fixture
+# Tests that some resources can have categories while others don't
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [
+ {
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ log_categories = ["AuditEvent"] # Has categories specified
+ },
+ {
+ log_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ metric_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ log_categories = [] # Empty array - enables all
+ },
+ {
+ log_namespace = "Microsoft.DBforPostgreSQL/flexibleServers"
+ metric_namespace = "Microsoft.DBforPostgreSQL/flexibleServers"
+ # log_categories omitted - enables all
+ }
+]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/multiple-resources-one-invalid.tfvars b/azure-collection-terraform/test/fixtures/multiple-resources-one-invalid.tfvars
new file mode 100644
index 00000000..d9473a00
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/multiple-resources-one-invalid.tfvars
@@ -0,0 +1,44 @@
+# Multiple resource types with one invalid category test fixture
+# Tests that one invalid category among multiple resources fails validation
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [
+ {
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ log_categories = ["AuditEvent"] # Valid
+ },
+ {
+ log_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ metric_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ log_categories = ["InvalidMySQLCategory"] # Invalid - should fail
+ }
+]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/multiple-resources-valid-categories.tfvars b/azure-collection-terraform/test/fixtures/multiple-resources-valid-categories.tfvars
new file mode 100644
index 00000000..2c28aa43
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/multiple-resources-valid-categories.tfvars
@@ -0,0 +1,44 @@
+# Multiple resource types with valid categories test fixture
+# Tests validation across multiple resource types
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [
+ {
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ log_categories = ["AuditEvent"]
+ },
+ {
+ log_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ metric_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ log_categories = ["MySqlAuditLogs", "MySqlSlowLogs"]
+ }
+]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/multiple-valid-log-categories.tfvars b/azure-collection-terraform/test/fixtures/multiple-valid-log-categories.tfvars
new file mode 100644
index 00000000..9a4701e1
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/multiple-valid-log-categories.tfvars
@@ -0,0 +1,37 @@
+# Multiple valid log categories test fixture
+# Tests that multiple valid categories pass validation
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [{
+ log_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ metric_namespace = "Microsoft.DBforMySQL/flexibleServers"
+ log_categories = ["MySqlAuditLogs", "MySqlSlowLogs"] # Multiple valid categories
+}]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/omitted-log-categories.tfvars b/azure-collection-terraform/test/fixtures/omitted-log-categories.tfvars
new file mode 100644
index 00000000..11e0d8b0
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/omitted-log-categories.tfvars
@@ -0,0 +1,37 @@
+# Omitted log categories test fixture
+# Tests that omitted log_categories field passes validation and enables all categories
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ # log_categories field omitted - should enable all available categories
+}]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/fixtures/region-specific-sku.tfvars b/azure-collection-terraform/test/fixtures/region-specific-sku.tfvars
new file mode 100644
index 00000000..7ca661df
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/region-specific-sku.tfvars
@@ -0,0 +1,46 @@
+# Test fixture for region-specific Event Hub SKU overrides
+# This demonstrates how to override the global SKU settings for specific regions
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-region-specific-sku-rg"
+location = "East US"
+eventhub_namespace_name = "test-region-sku-hub"
+policy_name = "TestRegionSpecificPolicy"
+
+# Global defaults: Standard SKU with 2 throughput units
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+
+# Override for East US: Use Premium SKU with 8 throughput units
+region_specific_eventhub_skus = {
+ "East US" = {
+ sku = "Premium"
+ throughput_units = 8
+ }
+}
+
+# Activity Log Configuration
+activity_log_export_name = "TestActivityLogExport"
+activity_log_export_category = "test/activity-logs"
+enable_activity_logs = false
+
+# Resource Configuration
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+}]
+
+nested_namespace_configs = {}
+required_resource_tags = {}
+prevent_deletion_if_contains_resources = false
+
+# Sumo Logic Configuration
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+installation_apps_list = []
diff --git a/azure-collection-terraform/test/fixtures/scan-interval-invalid-below-min.tfvars b/azure-collection-terraform/test/fixtures/scan-interval-invalid-below-min.tfvars
new file mode 100644
index 00000000..7c97b208
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/scan-interval-invalid-below-min.tfvars
@@ -0,0 +1,2 @@
+# Invalid scan_interval configuration - below minimum value (999ms < 1000ms)
+scan_interval = 999
diff --git a/azure-collection-terraform/test/fixtures/scan-interval-valid.tfvars b/azure-collection-terraform/test/fixtures/scan-interval-valid.tfvars
new file mode 100644
index 00000000..155c32b3
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/scan-interval-valid.tfvars
@@ -0,0 +1,2 @@
+# Valid scan_interval configuration with minimum value (1000ms = 1 second)
+scan_interval = 1000
diff --git a/azure-collection-terraform/test/fixtures/sumo-base-url-invalid-protocol.tfvars b/azure-collection-terraform/test/fixtures/sumo-base-url-invalid-protocol.tfvars
new file mode 100644
index 00000000..e8036e95
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-base-url-invalid-protocol.tfvars
@@ -0,0 +1,2 @@
+# Invalid sumologic_environment_base_url - missing https protocol
+sumologic_environment_base_url = "http://api.ch.sumologic.com/api/"
diff --git a/azure-collection-terraform/test/fixtures/sumo-base-url-valid.tfvars b/azure-collection-terraform/test/fixtures/sumo-base-url-valid.tfvars
new file mode 100644
index 00000000..4a635179
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-base-url-valid.tfvars
@@ -0,0 +1,2 @@
+# Valid sumologic_environment_base_url configuration for Switzerland deployment
+sumologic_environment_base_url = "https://api.ch.sumologic.com/api/"
diff --git a/azure-collection-terraform/test/fixtures/sumo-collector-dashes.tfvars b/azure-collection-terraform/test/fixtures/sumo-collector-dashes.tfvars
new file mode 100644
index 00000000..02c8bf36
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-collector-dashes.tfvars
@@ -0,0 +1,2 @@
+# Collector name with dashes - should be valid
+sumo_collector_name = "Test-Collector-With-Dashes"
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/sumo-empty-collector-name.tfvars b/azure-collection-terraform/test/fixtures/sumo-empty-collector-name.tfvars
new file mode 100644
index 00000000..6eb2ffd9
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-empty-collector-name.tfvars
@@ -0,0 +1,2 @@
+# Configuration with empty collector name (should fail)
+sumo_collector_name = ""
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/sumo-invalid-collector-name.tfvars b/azure-collection-terraform/test/fixtures/sumo-invalid-collector-name.tfvars
new file mode 100644
index 00000000..6e803ec0
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-invalid-collector-name.tfvars
@@ -0,0 +1,2 @@
+# Configuration for collector with special characters (should fail)
+sumo_collector_name = "Test@Collector#Name"
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/sumo-invalid-collector-special.tfvars b/azure-collection-terraform/test/fixtures/sumo-invalid-collector-special.tfvars
new file mode 100644
index 00000000..eca1b13a
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-invalid-collector-special.tfvars
@@ -0,0 +1,2 @@
+# Collector name with special characters - should fail validation
+sumo_collector_name = "Test@Collector#Name"
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/sumo-invalid-empty-apps.tfvars b/azure-collection-terraform/test/fixtures/sumo-invalid-empty-apps.tfvars
new file mode 100644
index 00000000..6834f2ac
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-invalid-empty-apps.tfvars
@@ -0,0 +1,19 @@
+# Invalid Sumo Logic apps configuration - empty app fields
+installation_apps_list = [
+ {
+ uuid = ""
+ name = ""
+ version = ""
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ },
+ {
+ uuid = "53376d23-2687-4500-b61e-4a2e2a119658"
+ name = "Azure Storage"
+ version = "1.0.3"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ }
+]
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/sumo-invalid-uuid.tfvars b/azure-collection-terraform/test/fixtures/sumo-invalid-uuid.tfvars
new file mode 100644
index 00000000..130e8f68
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-invalid-uuid.tfvars
@@ -0,0 +1,11 @@
+# Invalid Sumo Logic apps configuration - invalid UUID
+installation_apps_list = [
+ {
+ uuid = "invalid-uuid"
+ name = "Test App"
+ version = "1.0.0"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ }
+]
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/sumo-invalid-version.tfvars b/azure-collection-terraform/test/fixtures/sumo-invalid-version.tfvars
new file mode 100644
index 00000000..c0e5a7eb
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-invalid-version.tfvars
@@ -0,0 +1,11 @@
+# Invalid Sumo Logic apps configuration - invalid version
+installation_apps_list = [
+ {
+ uuid = "53376d23-2687-4500-b61e-4a2e2a119658"
+ name = "Test App"
+ version = "invalid"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+ }
+]
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/sumo-valid-apps.tfvars b/azure-collection-terraform/test/fixtures/sumo-valid-apps.tfvars
new file mode 100644
index 00000000..046a80f9
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/sumo-valid-apps.tfvars
@@ -0,0 +1,16 @@
+# Valid Sumo Logic apps configuration
+installation_apps_list = [{
+ uuid = "63e4bd6d-8e15-41c4-a65d-74589574adf2"
+ name = "Azure Load Balancer"
+ version = "1.0.4"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+},{
+ uuid = "53376d23-2687-4500-b61e-4a2e2a119658"
+ name = "Azure Storage"
+ version = "1.0.3"
+ parameters = {
+ "index_value" = "sumologic_default"
+ }
+}]
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/tag-filter-empty.tfvars b/azure-collection-terraform/test/fixtures/tag-filter-empty.tfvars
new file mode 100644
index 00000000..4e49ee29
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/tag-filter-empty.tfvars
@@ -0,0 +1,9 @@
+# Test fixture: Empty required_resource_tags map
+# Tests that empty map results in no filtering (discovers all resources)
+# Inherits other configuration from test.tfvars
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ required_resource_tags = {}
+}]
diff --git a/azure-collection-terraform/test/fixtures/tag-filter-multiple.tfvars b/azure-collection-terraform/test/fixtures/tag-filter-multiple.tfvars
new file mode 100644
index 00000000..09b9c3d8
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/tag-filter-multiple.tfvars
@@ -0,0 +1,13 @@
+# Test fixture: Multiple tags with AND logic
+# Tests that multiple tags use AND logic (all must match)
+# Inherits other configuration from test.tfvars
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ required_resource_tags = {
+ "environment" = "production"
+ "team" = "security"
+ "managed-by" = "terraform"
+ }
+}]
diff --git a/azure-collection-terraform/test/fixtures/tag-filter-omitted.tfvars b/azure-collection-terraform/test/fixtures/tag-filter-omitted.tfvars
new file mode 100644
index 00000000..d8752858
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/tag-filter-omitted.tfvars
@@ -0,0 +1,9 @@
+# Test fixture: Omitted required_resource_tags field
+# Tests that omitted field results in no filtering (discovers all resources)
+# Inherits other configuration from test.tfvars
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ # required_resource_tags field completely omitted - should discover all resources
+}]
diff --git a/azure-collection-terraform/test/fixtures/tag-filter-single.tfvars b/azure-collection-terraform/test/fixtures/tag-filter-single.tfvars
new file mode 100644
index 00000000..77e74f9d
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/tag-filter-single.tfvars
@@ -0,0 +1,11 @@
+# Test fixture: Single tag filter
+# Tests single tag filtering behavior
+# Inherits other configuration from test.tfvars
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ required_resource_tags = {
+ "environment" = "production"
+ }
+}]
diff --git a/azure-collection-terraform/test/fixtures/valid-config.tfvars b/azure-collection-terraform/test/fixtures/valid-config.tfvars
new file mode 100644
index 00000000..00d82286
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/valid-config.tfvars
@@ -0,0 +1,3 @@
+# Valid configuration with all required variables
+# This fixture uses only the base configuration from test.tfvars
+# No parameter overrides needed for basic validation test
\ No newline at end of file
diff --git a/azure-collection-terraform/test/fixtures/valid-log-categories.tfvars b/azure-collection-terraform/test/fixtures/valid-log-categories.tfvars
new file mode 100644
index 00000000..152bf44a
--- /dev/null
+++ b/azure-collection-terraform/test/fixtures/valid-log-categories.tfvars
@@ -0,0 +1,37 @@
+# Valid log categories test fixture
+# Tests that valid log categories pass validation
+
+azure_subscription_id = "c088dc46-d692-42ad-a4b6-9a542d28ad2a"
+azure_client_id = "694b6dc4-52e4-45c5-bd8f-4120b99d8eea"
+azure_client_secret = "test-secret"
+azure_tenant_id = "a39bedba-be8f-4c0f-bfe2-b8c7913501ea"
+
+resource_group_name = "test-rg"
+eventhub_namespace_name = "test-eventhub"
+eventhub_namespace_sku = "Standard"
+default_throughput_units = 2
+location = "East US"
+policy_name = "TestPolicy"
+
+
+enable_activity_logs = false
+activity_log_export_name = "TestActivityLog"
+activity_log_export_category = "azure/activity-logs"
+
+target_resource_types = [{
+ log_namespace = "Microsoft.KeyVault/vaults"
+ metric_namespace = "Microsoft.KeyVault/vaults"
+ log_categories = ["AuditEvent"] # Valid category for KeyVault
+}]
+
+required_resource_tags = {}
+nested_namespace_configs = {}
+
+sumologic_access_id = "test-access-id"
+sumologic_access_key = "test-access-key"
+sumologic_environment = "us1"
+sumo_collector_name = "test-collector"
+
+installation_apps_list = []
+
+prevent_deletion_if_contains_resources = true
diff --git a/azure-collection-terraform/test/go.mod b/azure-collection-terraform/test/go.mod
new file mode 100644
index 00000000..8e5d8e33
--- /dev/null
+++ b/azure-collection-terraform/test/go.mod
@@ -0,0 +1,75 @@
+module azure-collection-terraform-tests
+
+go 1.25
+
+require (
+ github.com/gruntwork-io/terratest v0.46.15
+ github.com/stretchr/testify v1.9.0
+)
+
+require (
+ cloud.google.com/go v0.112.1 // indirect
+ cloud.google.com/go/compute v1.25.1 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/iam v1.1.7 // indirect
+ cloud.google.com/go/storage v1.38.0 // indirect
+ github.com/agext/levenshtein v1.2.3 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/aws/aws-sdk-go v1.50.32 // indirect
+ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/s2a-go v0.1.7 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+ github.com/googleapis/gax-go/v2 v2.12.3 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+ github.com/hashicorp/go-getter v1.7.4 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-safetemp v1.0.0 // indirect
+ github.com/hashicorp/go-version v1.6.0 // indirect
+ github.com/hashicorp/hcl/v2 v2.20.0 // indirect
+ github.com/hashicorp/terraform-json v0.21.0 // indirect
+ github.com/jinzhu/copier v0.4.0 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/klauspost/compress v1.17.7 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/mattn/go-zglob v0.0.4 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/mitchellh/go-testing-interface v1.14.1 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/rogpeppe/go-internal v1.10.0 // indirect
+ github.com/tmccombs/hcl2json v0.6.2 // indirect
+ github.com/ulikunitz/xz v0.5.11 // indirect
+ github.com/zclconf/go-cty v1.14.4 // indirect
+ go.opencensus.io v0.24.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ go.opentelemetry.io/otel v1.24.0 // indirect
+ go.opentelemetry.io/otel/metric v1.24.0 // indirect
+ go.opentelemetry.io/otel/trace v1.24.0 // indirect
+ golang.org/x/crypto v0.21.0 // indirect
+ golang.org/x/mod v0.10.0 // indirect
+ golang.org/x/net v0.23.0 // indirect
+ golang.org/x/oauth2 v0.18.0 // indirect
+ golang.org/x/sync v0.6.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+ golang.org/x/tools v0.8.0 // indirect
+ google.golang.org/api v0.172.0 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/genproto v0.0.0-20240314234333-6e1732d8331c // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
+ google.golang.org/grpc v1.62.1 // indirect
+ google.golang.org/protobuf v1.33.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/azure-collection-terraform/test/go.sum b/azure-collection-terraform/test/go.sum
new file mode 100644
index 00000000..4bbddedb
--- /dev/null
+++ b/azure-collection-terraform/test/go.sum
@@ -0,0 +1,994 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
+cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
+cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
+cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
+cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
+cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
+cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
+cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
+cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
+cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=
+cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=
+cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=
+cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=
+cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=
+cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=
+cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=
+cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=
+cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=
+cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=
+cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=
+cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
+cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=
+cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
+cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=
+cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=
+cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=
+cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
+cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=
+cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
+cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
+cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
+cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
+cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
+cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
+cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
+cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
+cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
+cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=
+cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=
+cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=
+cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=
+cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
+cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=
+cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=
+cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=
+cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=
+cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=
+cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=
+cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=
+cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=
+cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=
+cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=
+cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=
+cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=
+cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=
+cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=
+cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=
+cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=
+cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=
+cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
+cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=
+cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=
+cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=
+cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=
+cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=
+cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=
+cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=
+cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
+cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
+cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
+cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
+cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
+cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
+cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
+cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
+cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
+cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
+cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
+cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
+cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=
+cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=
+cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=
+cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
+cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=
+cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=
+cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=
+cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=
+cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=
+cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=
+cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=
+cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=
+cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=
+cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=
+cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=
+cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=
+cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
+cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=
+cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=
+cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=
+cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=
+cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=
+cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=
+cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=
+cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=
+cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=
+cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=
+cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=
+cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=
+cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=
+cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=
+cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
+cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
+cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=
+cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=
+cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=
+cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=
+cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=
+cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
+cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
+cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
+cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
+cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg=
+cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY=
+cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
+cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=
+cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
+cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=
+cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
+cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=
+cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=
+cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=
+cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=
+cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
+cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
+github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
+github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/aws/aws-sdk-go v1.50.32 h1:POt81DvegnpQKM4DMDLlHz1CO6OBnEoQ1gRhYFd7QRY=
+github.com/aws/aws-sdk-go v1.50.32/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
+github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
+github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
+github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
+github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
+github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
+github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
+github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
+github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
+github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
+github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/gruntwork-io/terratest v0.46.15 h1:qfqjTFveymaqe7aAWn3LjlK0SwVGpRfoOut5ggNyfQ8=
+github.com/gruntwork-io/terratest v0.46.15/go.mod h1:9bd22zAojjBBiYdsp+AR1iyl2iB6bRUVm2Yf1AFhfrA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-getter v1.7.4 h1:3yQjWuxICvSpYwqSayAdKRFcvBl1y/vogCxczWSmix0=
+github.com/hashicorp/go-getter v1.7.4/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
+github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
+github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
+github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl/v2 v2.20.0 h1:l++cRs/5jQOiKVvqXZm/P1ZEfVXJmvLS9WSVxkaeTb4=
+github.com/hashicorp/hcl/v2 v2.20.0/go.mod h1:WmcD/Ym72MDOOx5F62Ly+leloeu6H7m0pG7VBiU6pQk=
+github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U=
+github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
+github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
+github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
+github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
+github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
+github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tmccombs/hcl2json v0.6.2 h1:x/QYPvPotKUgVyVhU6djucGBzVr3DZxw5AY9HkpFDiQ=
+github.com/tmccombs/hcl2json v0.6.2/go.mod h1:+Dm4GVB3QVIIB0lj+zmkOf86E3gJ12pRD9VvfsYj5ZE=
+github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
+github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
+github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
+go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
+golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
+golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
+golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
+golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
+golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
+google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
+google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
+google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
+google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
+google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
+google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
+google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
+google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
+google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
+google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
+google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
+google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
+google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
+google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
+google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
+google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
+google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
+google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk=
+google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
+google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
+google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
+google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
+google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
+google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
+google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
+google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
+google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
+google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
+google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
+google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
+google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
+google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
+google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
+google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
+google.golang.org/genproto v0.0.0-20240314234333-6e1732d8331c h1:1AVpelW1Ld8u6QbfPlwh00uAsR3xrnfn6FIJsCags3k=
+google.golang.org/genproto v0.0.0-20240314234333-6e1732d8331c/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U=
+google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc=
+google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/azure-collection-terraform/test/integration_test.go b/azure-collection-terraform/test/integration_test.go
new file mode 100644
index 00000000..7ea0c743
--- /dev/null
+++ b/azure-collection-terraform/test/integration_test.go
@@ -0,0 +1,452 @@
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gruntwork-io/terratest/modules/terraform"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// AzureResource represents an Azure resource for validation
+type AzureResource struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+// SumoLogicCollector represents a Sumo Logic collector
+type SumoLogicCollector struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+}
+
+// AppInstallationResult represents the result of app installation with error handling
+type AppInstallationResult struct {
+ Success bool
+ Message string
+ ErrorCode string
+ Scenario string
+}
+
+// TestAzureCollectionIntegration performs comprehensive end-to-end integration testing
+// with actual resource verification in both Azure and Sumo Logic, including app installation scenarios
+func TestAzureCollectionIntegration(t *testing.T) {
+ t.Log("🚀 Starting Comprehensive Azure Collection Integration Test...")
+
+ // Generate unique suffix for this test run to avoid resource conflicts
+ timestamp := time.Now().UnixNano() / int64(time.Millisecond)
+ uniqueSuffix := fmt.Sprintf("SumoTestActivityLogExport-%d", timestamp)
+
+ // Create terraform options with enhanced error handling for app installation scenarios
+ terraformOptions := &terraform.Options{
+ TerraformDir: "../", // Parent directory containing the terraform files
+ VarFiles: []string{
+ "test/test.tfvars", // Consolidated configuration file
+ },
+ Vars: map[string]interface{}{
+ // Make names unique per test run to avoid collisions with existing resources in the subscription
+ "activity_log_export_name": uniqueSuffix,
+ "resource_group_name": fmt.Sprintf("SUMO-%d-INTEGRATION-TEST", timestamp),
+ "eventhub_namespace_name": fmt.Sprintf("SUMO-%d-EventHub-test", timestamp),
+ },
+ EnvVars: map[string]string{
+ "TF_IN_AUTOMATION": "1",
+ },
+ NoColor: true,
+ }
+
+ // Defer cleanup to ensure resources are always destroyed
+ defer func() {
+ t.Log("🧹 Starting resource cleanup...")
+ destroyWithAppHandling(t, terraformOptions)
+ t.Log("✅ Resource cleanup completed")
+ }()
+
+ // Apply the Terraform configuration with app installation error handling
+ t.Log("📦 Applying Terraform configuration...")
+ applyWithAppHandling(t, terraformOptions)
+
+ // Get Terraform outputs for validation
+ t.Log("📋 Retrieving Terraform outputs...")
+ resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
+ collectorID := terraform.Output(t, terraformOptions, "sumologic_collector_id")
+ logSourceIDs := terraform.OutputMap(t, terraformOptions, "sumologic_log_source_ids")
+ metricsSourceID := terraform.Output(t, terraformOptions, "sumologic_metrics_source_id")
+
+ // Validate that we got the expected outputs
+ require.NotEmpty(t, resourceGroupName, "Resource group name should not be empty")
+ require.NotEmpty(t, collectorID, "Sumo Logic collector ID should not be empty")
+ require.NotEmpty(t, logSourceIDs, "Sumo Logic log source IDs should not be empty")
+ require.NotEmpty(t, metricsSourceID, "Sumo Logic metrics source ID should not be empty")
+
+ t.Logf("📊 Terraform Outputs Retrieved:")
+ t.Logf(" - Resource Group: %s", resourceGroupName)
+ t.Logf(" - Collector ID: %s", collectorID)
+ t.Logf(" - Metrics Source ID: %s", metricsSourceID)
+ t.Logf(" - Log Source IDs: %v", logSourceIDs)
+
+ // Phase 1: Verify Azure Resources
+ t.Log("🔍 Phase 1: Verifying Azure Resources...")
+ // Derive expected EventHubs from terraform output 'eventhub_names' so tests follow actual config
+ eventhubNamesMap := terraform.OutputMap(t, terraformOptions, "eventhub_names")
+ verifyAzureResources(t, resourceGroupName, eventhubNamesMap)
+
+ // Phase 2: Verify Sumo Logic Resources
+ t.Log("🔍 Phase 2: Verifying Sumo Logic Resources...")
+ verifySumoLogicResources(t, collectorID, logSourceIDs, metricsSourceID)
+
+ // Phase 3: Verify App Installation Status
+ t.Log("🔍 Phase 3: Verifying App Installation Status...")
+ verifyAppInstallationStatus(t, terraformOptions)
+
+ // Phase 4: Verify Diagnostic Settings
+ t.Log("🔍 Phase 4: Verifying Diagnostic Settings...")
+ verifyDiagnosticSettings(t, resourceGroupName)
+
+ t.Log("✅ All integration tests passed successfully!")
+}
+
+// verifyAzureResources validates that Azure resources are properly created
+func verifyAzureResources(t *testing.T, resourceGroupName string, expectedEventhubMap map[string]string) {
+ t.Log("🔍 Verifying Azure Resource Group...")
+
+ // Verify Resource Group exists
+ cmd := exec.Command("az", "group", "show", "--name", resourceGroupName, "--output", "json")
+ output, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("❌ Failed to find Azure Resource Group '%s': %v", resourceGroupName, err)
+ }
+
+ var rg AzureResource
+ err = json.Unmarshal(output, &rg)
+ require.NoError(t, err, "Failed to parse Resource Group JSON")
+ assert.Equal(t, resourceGroupName, rg.Name, "Resource Group name mismatch")
+ t.Logf("✅ Resource Group '%s' verified", resourceGroupName)
+
+ // Verify EventHub Namespaces exist - Query ALL namespaces in the resource group
+ t.Log("🔍 Verifying EventHub Namespaces...")
+ cmd = exec.Command("az", "eventhubs", "namespace", "list",
+ "--resource-group", resourceGroupName,
+ "--output", "json")
+ output, err = cmd.Output()
+ if err != nil {
+ t.Fatalf("❌ Failed to list EventHub Namespaces: %v", err)
+ }
+
+ var namespaces []AzureResource
+ err = json.Unmarshal(output, &namespaces)
+ require.NoError(t, err, "Failed to parse EventHub Namespaces JSON")
+
+ if len(namespaces) == 0 {
+ t.Fatalf("❌ No EventHub Namespaces found in resource group '%s'", resourceGroupName)
+ }
+
+ t.Logf("✅ Found %d EventHub Namespace(s)", len(namespaces))
+ for _, ns := range namespaces {
+ t.Logf(" - %s", ns.Name)
+ }
+
+ // Collect EventHubs from ALL namespaces
+ t.Log("🔍 Verifying EventHubs across all namespaces...")
+ allEventHubNames := []string{}
+
+ for _, ns := range namespaces {
+ cmd = exec.Command("az", "eventhubs", "eventhub", "list",
+ "--resource-group", resourceGroupName,
+ "--namespace-name", ns.Name,
+ "--output", "json")
+ output, err = cmd.Output()
+ if err != nil {
+ t.Logf("⚠️ Failed to list EventHubs in namespace '%s': %v", ns.Name, err)
+ continue
+ }
+
+ var eventhubs []AzureResource
+ err = json.Unmarshal(output, &eventhubs)
+ if err != nil {
+ t.Logf("⚠️ Failed to parse EventHubs JSON for namespace '%s': %v", ns.Name, err)
+ continue
+ }
+
+ for _, eh := range eventhubs {
+ allEventHubNames = append(allEventHubNames, eh.Name)
+ t.Logf(" - Found EventHub '%s' in namespace '%s'", eh.Name, ns.Name)
+ }
+ }
+
+ // If Terraform produced no expected mapping, fall back to asserting that at least one EventHub exists
+ if len(expectedEventhubMap) == 0 {
+ assert.True(t, len(allEventHubNames) > 0, "No EventHubs found and no expected EventHubs were provided")
+ t.Logf("✅ Found %d EventHubs: %v", len(allEventHubNames), allEventHubNames)
+ return
+ }
+
+ // For each expected eventhub name from Terraform, assert it exists in the combined EventHub list from all namespaces
+ for _, expectedName := range expectedEventhubMap {
+ found := false
+ for _, ehName := range allEventHubNames {
+ // Case-insensitive comparison to handle Azure API returning lowercase names
+ if strings.EqualFold(ehName, expectedName) {
+ found = true
+ break
+ }
+ }
+ assert.True(t, found, "Missing EventHub '%s'. Available EventHubs: %v", expectedName, allEventHubNames)
+ }
+
+ t.Logf("✅ All %d EventHubs verified: %v", len(allEventHubNames), allEventHubNames)
+}
+
+// verifySumoLogicResources validates that Sumo Logic resources are properly created
+func verifySumoLogicResources(t *testing.T, collectorID string, logSourceIDs map[string]string, metricsSourceID string) {
+ // Note: This is a simplified verification since we don't have direct Sumo Logic API access in the test
+ // In a real implementation, you would use the Sumo Logic API to verify these resources
+
+ t.Log("🔍 Verifying Sumo Logic Collector...")
+ assert.NotEmpty(t, collectorID, "Collector ID should not be empty")
+ assert.Regexp(t, `^\d+$`, collectorID, "Collector ID should be numeric")
+ t.Logf("✅ Sumo Logic Collector ID '%s' format verified", collectorID)
+
+ t.Log("🔍 Verifying Sumo Logic Log Sources...")
+ // Dynamically verify whatever log sources Terraform reported in the outputs
+ if len(logSourceIDs) == 0 {
+ t.Log("ℹ️ No log sources reported by Terraform outputs; skipping detailed log source verification")
+ } else {
+ for expected, sourceID := range logSourceIDs {
+ assert.NotEmpty(t, expected, "Log source key should not be empty")
+ assert.NotEmpty(t, sourceID, "Log source ID should not be empty for: %s", expected)
+ assert.Regexp(t, `^\d+$`, sourceID, "Log source ID should be numeric for: %s", expected)
+ t.Logf("✅ Log Source '%s' ID '%s' verified", expected, sourceID)
+ }
+ }
+
+ t.Log("🔍 Verifying Sumo Logic Metrics Source...")
+ assert.NotEmpty(t, metricsSourceID, "Metrics source ID should not be empty")
+ assert.Regexp(t, `^\d+$`, metricsSourceID, "Metrics source ID should be numeric")
+ t.Logf("✅ Sumo Logic Metrics Source ID '%s' verified", metricsSourceID)
+}
+
+// verifyAppInstallationStatus verifies the status of installed Sumo Logic apps
+func verifyAppInstallationStatus(t *testing.T, options *terraform.Options) {
+ // Get the installed apps output from terraform
+ installedAppsOutput := terraform.Output(t, options, "installed_apps")
+
+ if installedAppsOutput == "{}" || installedAppsOutput == "" {
+ t.Log("ℹ️ No apps configured for installation (installation_apps_list is empty)")
+ return
+ }
+
+ t.Logf("📱 Installed Apps Status: %s", installedAppsOutput)
+
+ // In a real implementation, you might parse the installed_apps output
+ // and validate each app's installation status
+ // For now, we'll just log the status
+
+ // Try to get more detailed app information if available
+ outputs, err := terraform.OutputAllE(t, options)
+ if err != nil {
+ t.Logf("⚠️ Could not retrieve all terraform outputs for app verification: %v", err)
+ return
+ }
+
+ // Check if there are any app-related outputs
+ appOutputsFound := false
+ for key, value := range outputs {
+ if strings.Contains(strings.ToLower(key), "app") {
+ t.Logf("📱 App Output '%s': %s", key, value)
+ appOutputsFound = true
+ }
+ }
+
+ if !appOutputsFound {
+ t.Log("ℹ️ No app-specific outputs found - apps may not be configured or may have encountered installation scenarios")
+ } else {
+ t.Log("✅ App installation status verified")
+ }
+}
+
+// verifyDiagnosticSettings validates that diagnostic settings are properly configured
+func verifyDiagnosticSettings(t *testing.T, resourceGroupName string) {
+ t.Log("🔍 Verifying Diagnostic Settings...")
+
+ // Get list of diagnostic settings (this will show if any are configured)
+ cmd := exec.Command("az", "monitor", "diagnostic-settings", "list",
+ "--resource-group", resourceGroupName,
+ "--output", "json")
+ output, err := cmd.Output()
+
+ if err != nil {
+ // If the command fails, it might be because there are no diagnostic settings
+ // or because of permission issues, but we should not fail the test for this
+ t.Logf("⚠️ Could not verify diagnostic settings: %v", err)
+ return
+ }
+
+ // Parse and validate diagnostic settings if we can get them
+ if len(output) > 0 && !strings.Contains(string(output), "[]") {
+ t.Log("✅ Diagnostic settings are configured (detailed validation would require resource-specific queries)")
+ } else {
+ t.Log("ℹ️ No diagnostic settings found or query returned empty results")
+ }
+}
+
+// applyWithAppHandling applies terraform configuration with enhanced error handling for app installation scenarios
+func applyWithAppHandling(t *testing.T, options *terraform.Options) {
+ terraform.Init(t, options)
+
+ // Try to apply, but handle app-related errors gracefully
+ _, err := terraform.ApplyE(t, options)
+ if err != nil {
+ errorStr := err.Error()
+ appResults := analyzeAppInstallationErrors(errorStr)
+
+ if len(appResults) > 0 {
+ t.Log("📱 App Installation Scenarios Detected:")
+ for _, result := range appResults {
+ t.Logf(" - %s: %s", result.Scenario, result.Message)
+ }
+
+ // If all errors are app-related and are valid scenarios, continue with the test
+ if allErrorsAreValidAppScenarios(appResults, errorStr) {
+ t.Log("ℹ️ All errors are valid app installation scenarios, continuing with infrastructure validation")
+ // Force apply ignoring app errors by retrying
+ terraform.Apply(t, options)
+ return
+ }
+ }
+
+ // If there are non-app related errors, fail the test
+ t.Fatalf("❌ Terraform apply failed with non-app errors: %v", err)
+ }
+
+ t.Log("✅ Terraform configuration applied successfully")
+}
+
+// destroyWithAppHandling destroys terraform resources with enhanced error handling for app scenarios
+func destroyWithAppHandling(t *testing.T, options *terraform.Options) {
+ _, err := terraform.DestroyE(t, options)
+ if err != nil {
+ errorStr := err.Error()
+ appResults := analyzeAppUninstallationErrors(errorStr)
+
+ if len(appResults) > 0 {
+ t.Log("📱 App Uninstallation Scenarios Detected:")
+ for _, result := range appResults {
+ t.Logf(" - %s: %s", result.Scenario, result.Message)
+ }
+
+ // If all errors are app-related "not found" scenarios, consider it successful
+ if allErrorsAreValidAppUninstallScenarios(appResults, errorStr) {
+ t.Log("ℹ️ All errors are valid app uninstallation scenarios (apps already removed manually)")
+ return
+ }
+ }
+
+ // If there are non-app related errors, fail the cleanup but don't fail the test
+ t.Logf("⚠️ Terraform destroy encountered non-app errors (resources may need manual cleanup): %v", err)
+ return
+ }
+
+ t.Log("✅ Terraform resources destroyed successfully")
+}
+
+// analyzeAppInstallationErrors analyzes terraform errors for app installation scenarios
+func analyzeAppInstallationErrors(errorStr string) []AppInstallationResult {
+ var results []AppInstallationResult
+
+ // Check for "app already installed" scenario
+ if strings.Contains(errorStr, "apps:app_already_installed") ||
+ strings.Contains(errorStr, "App with given uuid is already installed") {
+ results = append(results, AppInstallationResult{
+ Success: false,
+ Message: "App is already installed manually or via another process",
+ ErrorCode: "apps:app_already_installed",
+ Scenario: "App Already Installed",
+ })
+ }
+
+ // Check for other app-related installation errors
+ if strings.Contains(errorStr, "apps:") && !strings.Contains(errorStr, "apps:app_already_installed") {
+ results = append(results, AppInstallationResult{
+ Success: false,
+ Message: "App installation error detected",
+ ErrorCode: "apps:general_error",
+ Scenario: "App Installation Error",
+ })
+ }
+
+ return results
+}
+
+// analyzeAppUninstallationErrors analyzes terraform errors for app uninstallation scenarios
+func analyzeAppUninstallationErrors(errorStr string) []AppInstallationResult {
+ var results []AppInstallationResult
+
+ // Check for "app not found" scenario during destroy
+ if strings.Contains(errorStr, "apps:not_found") ||
+ strings.Contains(errorStr, "App instance with given id not found") {
+ results = append(results, AppInstallationResult{
+ Success: true, // This is actually a valid scenario
+ Message: "App was already uninstalled manually (not found in Sumo Logic)",
+ ErrorCode: "apps:not_found",
+ Scenario: "App Already Uninstalled",
+ })
+ }
+
+ // Check for Azure Backup lock scenario during destroy
+ if strings.Contains(errorStr, "ScopeLocked") ||
+ strings.Contains(errorStr, "AzureBackupProtectionLock") {
+ results = append(results, AppInstallationResult{
+ Success: true, // This is an acceptable scenario in production
+ Message: "Resources are protected by Azure Backup locks (cannot delete diagnostic settings)",
+ ErrorCode: "azure:scope_locked",
+ Scenario: "Azure Backup Protection",
+ })
+ }
+
+ return results
+}
+
+// allErrorsAreValidAppScenarios checks if all errors in the terraform output are valid app scenarios
+func allErrorsAreValidAppScenarios(appResults []AppInstallationResult, fullError string) bool {
+ if len(appResults) == 0 {
+ return false
+ }
+
+ // Check if the error is predominantly app-related
+ // This is a simple heuristic - in a more sophisticated implementation,
+ // you might parse the full terraform error more thoroughly
+ appErrorIndicators := []string{
+ "apps:app_already_installed",
+ "App with given uuid is already installed",
+ "sumologic_app",
+ }
+
+ hasAppErrors := false
+ for _, indicator := range appErrorIndicators {
+ if strings.Contains(fullError, indicator) {
+ hasAppErrors = true
+ break
+ }
+ }
+
+ return hasAppErrors
+}
+
+// allErrorsAreValidAppUninstallScenarios checks if all destroy errors are valid app uninstall scenarios
+func allErrorsAreValidAppUninstallScenarios(appResults []AppInstallationResult, fullError string) bool {
+ // If we have app results indicating "not found" scenarios, and no other critical errors
+ for _, result := range appResults {
+ if result.ErrorCode == "apps:not_found" && result.Success {
+ return true
+ }
+ }
+ return false
+}
diff --git a/azure-collection-terraform/test/log_categories_validation_test.go b/azure-collection-terraform/test/log_categories_validation_test.go
new file mode 100644
index 00000000..20728b49
--- /dev/null
+++ b/azure-collection-terraform/test/log_categories_validation_test.go
@@ -0,0 +1,236 @@
+package test
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/gruntwork-io/terratest/modules/terraform"
+ "github.com/stretchr/testify/assert"
+)
+
+// TestLogCategoriesValidation tests the validation of log_categories field in target_resource_types
+func TestLogCategoriesValidation(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ errorMsg string
+ }{
+ {
+ name: "ValidLogCategories",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-log-categories.tfvars"),
+ expectError: false,
+ description: "Valid log categories should pass validation",
+ },
+ {
+ name: "InvalidLogCategory",
+ tfvarsFile: filepath.Join("test", fixturesDir, "invalid-log-category.tfvars"),
+ expectError: true,
+ description: "Invalid log category should fail validation during plan phase",
+ errorMsg: "Invalid category",
+ },
+ {
+ name: "EmptyLogCategories",
+ tfvarsFile: filepath.Join("test", fixturesDir, "empty-log-categories.tfvars"),
+ expectError: false,
+ description: "Empty log_categories array should pass validation (enables all categories)",
+ },
+ {
+ name: "OmittedLogCategories",
+ tfvarsFile: filepath.Join("test", fixturesDir, "omitted-log-categories.tfvars"),
+ expectError: false,
+ description: "Omitted log_categories field should pass validation (enables all categories)",
+ },
+ {
+ name: "MultipleValidLogCategories",
+ tfvarsFile: filepath.Join("test", fixturesDir, "multiple-valid-log-categories.tfvars"),
+ expectError: false,
+ description: "Multiple valid log categories should pass validation",
+ },
+ {
+ name: "MixedValidAndInvalidCategories",
+ tfvarsFile: filepath.Join("test", fixturesDir, "mixed-log-categories.tfvars"),
+ expectError: true,
+ description: "Mix of valid and invalid categories should fail validation",
+ errorMsg: "Invalid category",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tt.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ if tt.expectError {
+ assert.Error(t, err, tt.description)
+ if err != nil && tt.errorMsg != "" {
+ errStr := err.Error()
+ assert.Contains(t, errStr, tt.errorMsg,
+ fmt.Sprintf("Error message should contain '%s', got: %v", tt.errorMsg, err))
+
+ // Verify it's a precondition error (validation happens during plan)
+ assert.True(t,
+ strings.Contains(errStr, "Resource precondition failed") ||
+ strings.Contains(errStr, "Invalid log categories detected"),
+ "Should be a precondition validation error, got: %v", err)
+ }
+ } else {
+ // For valid configurations, validation should pass
+ // We might get API errors if resources don't exist, but validation itself should pass
+ if err != nil {
+ errStr := err.Error()
+ if strings.Contains(errStr, "Invalid category") ||
+ strings.Contains(errStr, "Invalid log categories detected") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tt.name, err)
+ } else {
+ // API errors are expected for validation-only tests without real resources
+ t.Logf("Test case '%s' passed validation but failed at runtime (expected): %v", tt.name, err)
+ }
+ }
+ }
+ })
+ }
+}
+
+// TestLogCategoriesValidationWithMultipleResourceTypes tests log categories validation
+// across multiple resource types in the same configuration
+func TestLogCategoriesValidationWithMultipleResourceTypes(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "MultipleResourceTypesAllValid",
+ tfvarsFile: filepath.Join("test", fixturesDir, "multiple-resources-valid-categories.tfvars"),
+ expectError: false,
+ description: "Multiple resource types with valid categories should pass",
+ },
+ {
+ name: "MultipleResourceTypesOneInvalid",
+ tfvarsFile: filepath.Join("test", fixturesDir, "multiple-resources-one-invalid.tfvars"),
+ expectError: true,
+ description: "Multiple resource types with one having invalid category should fail",
+ },
+ {
+ name: "MultipleResourceTypesMixedCategories",
+ tfvarsFile: filepath.Join("test", fixturesDir, "multiple-resources-mixed-categories.tfvars"),
+ expectError: false,
+ description: "Multiple resource types with some having categories and others without should pass",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ })
+ }
+}
+
+// TestLogCategoriesErrorMessages verifies that validation error messages are helpful and clear
+func TestLogCategoriesErrorMessages(t *testing.T) {
+ terraformOptions := createTerraformOptions(
+ filepath.Join("test", fixturesDir, "invalid-log-category.tfvars"),
+ )
+
+ terraform.Init(t, terraformOptions)
+
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ assert.Error(t, err, "Should fail with invalid log category")
+
+ if err != nil {
+ errStr := err.Error()
+
+ // Error message should contain:
+ // 1. The invalid category name
+ // 2. The resource type
+ // 3. List of valid categories
+ assert.Contains(t, errStr, "Invalid category", "Error should mention invalid category")
+ assert.Contains(t, errStr, "Valid categories are:", "Error should list valid categories")
+
+ t.Logf("Error message (as expected): %s", errStr)
+ }
+}
+
+// TestLogCategoriesDynamicValidation tests that validation uses actual Azure diagnostic categories
+// rather than static lists
+func TestLogCategoriesDynamicValidation(t *testing.T) {
+ t.Log("Testing that log_categories validation is dynamic and fetches from Azure...")
+
+ // This test verifies that the validation logic queries Azure for valid categories
+ // rather than using a hardcoded list
+
+ terraformOptions := createTerraformOptions(
+ filepath.Join("test", fixturesDir, "valid-log-categories.tfvars"),
+ )
+
+ terraform.Init(t, terraformOptions)
+
+ // Run plan - this should query Azure diagnostic categories
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ // We expect this might fail due to API access issues in test environment,
+ // but we can verify the data sources are being used
+ if err != nil {
+ errStr := err.Error()
+
+ // Check if error is related to querying diagnostic categories
+ // (which means the dynamic lookup is happening)
+ if strings.Contains(errStr, "azurerm_monitor_diagnostic_categories") {
+ t.Log("✅ Confirmed: Validation uses dynamic category lookup from Azure")
+ return
+ }
+
+ // If we get a different error, log it but don't fail
+ // (might be authentication or other test environment issues)
+ t.Logf("Got error during plan (may be expected in test environment): %v", err)
+ } else {
+ t.Log("✅ Plan succeeded - validation passed")
+ }
+}
+
+// TestLogCategoriesOptimization verifies that the validation uses efficient data source queries
+func TestLogCategoriesOptimization(t *testing.T) {
+ t.Log("Verifying that log_categories validation uses optimized data source queries...")
+
+ // This test checks that we're using type_categories (efficient)
+ // rather than all_categories (inefficient) for validation
+
+ terraformOptions := createTerraformOptions(
+ filepath.Join("test", fixturesDir, "multiple-resources-valid-categories.tfvars"),
+ )
+
+ terraform.Init(t, terraformOptions)
+
+ // Show the plan output to verify data source usage
+ output, err := terraform.RunTerraformCommandE(t, terraformOptions, "plan", "-no-color")
+
+ // Check the output for data source queries
+ if err == nil || output != "" {
+ // Verify that type_categories data source is mentioned
+ if strings.Contains(output, "data.azurerm_monitor_diagnostic_categories.type_categories") {
+ t.Log("✅ Confirmed: Using optimized type_categories data source")
+ }
+
+ // Log for manual verification
+ t.Logf("Plan output (first 500 chars): %s", output[:min(500, len(output))])
+ } else if err != nil {
+ t.Logf("Plan error (may be expected in test environment): %v", err)
+ }
+}
+
+// Helper function to get minimum of two integers
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
diff --git a/azure-collection-terraform/test/sumologic_test.go b/azure-collection-terraform/test/sumologic_test.go
new file mode 100644
index 00000000..e9058e7a
--- /dev/null
+++ b/azure-collection-terraform/test/sumologic_test.go
@@ -0,0 +1,1134 @@
+package test
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/gruntwork-io/terratest/modules/terraform"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// stripANSI removes ANSI escape sequences from a string for cleaner plan output validation
+func stripANSI(str string) string {
+ ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
+ return ansiRegex.ReplaceAllString(str, "")
+}
+
+// Uses the same helper functions as azure_test.go - clean tfvars-based approach
+
+func TestSumoLogicResourceTypesValidation(t *testing.T) {
+ // Test cases for Sumo Logic apps/resources
+ testCases := []struct {
+ name string
+ tfvarsFile string
+ shouldPass bool
+ description string
+ }{
+ {
+ name: "ValidApps",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-valid-apps.tfvars"),
+ shouldPass: true,
+ description: "Valid UUIDs, names, and versions should pass validation",
+ },
+ {
+ name: "InvalidEmptyApps",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-invalid-empty-apps.tfvars"),
+ shouldPass: false,
+ description: "Empty UUIDs, names, and versions should fail validation",
+ },
+ {
+ name: "InvalidUUID",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-invalid-uuid.tfvars"),
+ shouldPass: false,
+ description: "Invalid UUID format should fail validation",
+ },
+ {
+ name: "InvalidVersion",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-invalid-version.tfvars"),
+ shouldPass: false,
+ description: "Invalid semantic version should fail validation",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ runValidationTest(t, tc.name, tc.tfvarsFile, !tc.shouldPass, tc.description)
+ })
+ }
+}
+
+func TestSumoLogicCollectorResourceConfiguration(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidCollectorConfiguration",
+ tfvarsFile: filepath.Join("test", fixturesDir, "valid-config.tfvars"),
+ expectError: false,
+ description: "Valid collector with proper naming",
+ },
+ {
+ name: "CollectorNameWithSpecialChars",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-invalid-collector-name.tfvars"),
+ expectError: true,
+ description: "Collector name with special characters should fail validation",
+ },
+ {
+ name: "EmptyCollectorName",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-empty-collector-name.tfvars"),
+ expectError: true,
+ description: "Empty collector name should fail validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runValidationTest(t, tt.name, tt.tfvarsFile, tt.expectError, tt.description)
+ })
+ }
+}
+
+func TestSumoLogicEventHubLogSourceConfiguration(t *testing.T) {
+ // Test Event Hub log source configuration
+ terraformOptions := createTerraformOptions(filepath.Join("test", fixturesDir, "valid-config.tfvars"))
+
+ terraform.Init(t, terraformOptions)
+ plan, err := terraform.PlanE(t, terraformOptions)
+
+ // We expect this might fail with API errors, but should not fail with validation errors
+ if err != nil {
+ errStr := err.Error()
+ assert.False(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule"),
+ "Should not have validation errors: %v", err)
+
+ t.Logf("Event Hub log source test passed validation but failed at runtime (expected): %v", err)
+ // Even with authentication failure, we can still validate the plan structure
+ // by checking if the error occurred after plan generation
+ if strings.Contains(errStr, "Terraform planned the following actions") {
+ t.Logf("Plan was generated before authentication failure, proceeding with validation")
+ // Extract plan content from error for validation
+ planContent := errStr
+ validateEventHubPlanContent(t, planContent)
+ }
+ return
+ }
+
+ // If no error, validate the successful plan
+ validateEventHubPlanContent(t, plan)
+}
+
+// Helper function to validate Event Hub plan content
+func validateEventHubPlanContent(t *testing.T, planContent string) {
+ // Define comprehensive expected content patterns with correct resource names
+ expectedPatterns := []struct {
+ pattern string
+ description string
+ required bool // Some patterns might not appear if no Azure resources are discovered
+ }{
+ {
+ pattern: `azurerm_eventhub\s*\.\s*eventhubs_by_type_and_location`,
+ description: "Event Hub resource should be defined",
+ required: false, // Depends on Azure resource discovery
+ },
+ {
+ pattern: `azurerm_eventhub_namespace\s*\.\s*namespaces_by_location`,
+ description: "Event Hub namespace should be defined",
+ required: false, // Depends on Azure resource discovery
+ },
+ {
+ pattern: `sumologic_azure_event_hub_log_source\s*\.\s*sumo_azure_event_hub_log_source`,
+ description: "Sumo Logic Azure Event Hub log source should be defined",
+ required: false, // Depends on Azure resource discovery
+ },
+ {
+ pattern: `sumologic_collector\s*\.\s*sumo_collector`,
+ description: "Sumo Logic collector should always be defined",
+ required: true, // Always created regardless of Azure resources
+ },
+ {
+ pattern: `name\s*=\s*".*Collector.*"`,
+ description: "Collector should have name containing 'Collector'",
+ required: true,
+ },
+ {
+ pattern: `content_type\s*=\s*"AzureEventHubLog"`,
+ description: "Event Hub log sources should have correct content type",
+ required: false, // Only present when Event Hub sources exist
+ },
+ {
+ pattern: `category\s*=\s*"azure/logs/`,
+ description: "Event Hub log sources should have azure logs category",
+ required: false, // Only present when Event Hub sources exist
+ },
+ {
+ pattern: `type\s*=\s*"AzureEventHubAuthentication"`,
+ description: "Event Hub sources should use proper authentication type",
+ required: false, // Only present when Event Hub sources exist
+ },
+ }
+
+ // Test each expected pattern
+ for _, expected := range expectedPatterns {
+ matched, err := regexp.MatchString(expected.pattern, planContent)
+ require.NoError(t, err, "Failed to compile regex pattern: %s", expected.pattern)
+
+ if expected.required {
+ assert.True(t, matched, expected.description+". Pattern: %s", expected.pattern)
+ if matched {
+ t.Logf("✓ %s", expected.description)
+ }
+ } else {
+ // For optional patterns, log whether they were found
+ if matched {
+ t.Logf("✓ %s", expected.description)
+ } else {
+ t.Logf("- %s (not found - depends on Azure resource discovery)", expected.description)
+ }
+ }
+ }
+
+ // Count actual resources in plan
+ collectorMatches := regexp.MustCompile(`sumologic_collector\s*\.\s*sumo_collector`).FindAllString(planContent, -1)
+ eventHubMatches := regexp.MustCompile(`azurerm_eventhub\s*\.\s*eventhubs_by_type_and_location`).FindAllString(planContent, -1)
+ logSourceMatches := regexp.MustCompile(`sumologic_azure_event_hub_log_source\s*\.\s*sumo_azure_event_hub_log_source`).FindAllString(planContent, -1)
+
+ // Collector should always be present
+ assert.GreaterOrEqual(t, len(collectorMatches), 1, "Should have at least 1 Sumo Logic collector")
+
+ // Event Hubs and log sources depend on Azure resource discovery
+ if len(eventHubMatches) > 0 {
+ t.Logf("Found %d Event Hub resources", len(eventHubMatches))
+ assert.GreaterOrEqual(t, len(logSourceMatches), 1, "Should have Sumo Logic log sources when Event Hubs exist")
+ } else {
+ t.Logf("No Event Hub resources found (expected in test environment without real Azure resources)")
+ }
+
+ t.Logf("Event Hub Log Source configuration validation completed. Found: %d collectors, %d Event Hubs, %d log sources",
+ len(collectorMatches), len(eventHubMatches), len(logSourceMatches))
+}
+
+func TestSumoLogicActivityLogSourceConfiguration(t *testing.T) {
+ // Test Activity Log source configuration with both enabled and disabled scenarios
+ testCases := []struct {
+ name string
+ tfvarsFile string
+ activityLogsEnabled bool
+ expectedResourceCount int
+ description string
+ }{
+ {
+ name: "ActivityLogsEnabled",
+ tfvarsFile: filepath.Join("test", fixturesDir, "activity-logs-enabled.tfvars"),
+ activityLogsEnabled: true,
+ expectedResourceCount: 16, // 11 base resources + 5 activity log resources
+ description: "Activity logs enabled should create dedicated Activity Log infrastructure",
+ },
+ {
+ name: "ActivityLogsDisabled",
+ tfvarsFile: filepath.Join("test", fixturesDir, "activity-logs-disabled.tfvars"),
+ activityLogsEnabled: false,
+ expectedResourceCount: 11, // Only base resources (Event Hubs, collector, etc.)
+ description: "Activity logs disabled should not create Activity Log infrastructure",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tc.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+ plan, err := terraform.PlanE(t, terraformOptions)
+
+ // We expect this might fail with API errors, but should not fail with validation errors
+ if err != nil {
+ errStr := err.Error()
+ assert.False(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule"),
+ "Should not have validation errors: %v", err)
+
+ t.Logf("%s test passed validation but failed at runtime (expected): %v", tc.name, err)
+
+ // Even with authentication failure, we can still validate the plan structure
+ if strings.Contains(errStr, "Terraform planned the following actions") {
+ t.Logf("Plan was generated before authentication failure, proceeding with validation")
+ validateActivityLogPlanContent(t, errStr, tc.activityLogsEnabled)
+ }
+ return
+ }
+
+ // If no error, validate the successful plan
+ validateActivityLogPlanContent(t, plan, tc.activityLogsEnabled)
+ })
+ }
+}
+
+// Helper function to validate Activity Log plan content
+func validateActivityLogPlanContent(t *testing.T, planContent string, activityLogsEnabled bool) {
+ // Define base patterns that should always be present
+ basePatterns := []struct {
+ pattern string
+ description string
+ required bool
+ }{
+ {
+ pattern: `sumologic_collector\s*\.\s*sumo_collector`,
+ description: "Sumo Logic collector should always be defined",
+ required: true,
+ },
+ {
+ pattern: `name\s*=\s*".*Collector.*"`,
+ description: "Collector should have name containing 'Collector'",
+ required: true,
+ },
+ {
+ pattern: `azurerm_eventhub\s*\.\s*eventhubs_by_type_and_location`,
+ description: "Resource-specific Event Hubs should be defined",
+ required: false, // Depends on Azure resource discovery
+ },
+ {
+ pattern: `sumologic_azure_event_hub_log_source\s*\.\s*sumo_azure_event_hub_log_source`,
+ description: "Resource-specific Event Hub log sources should be defined",
+ required: false, // Depends on Azure resource discovery
+ },
+ }
+
+ // Define Activity Log-specific patterns
+ activityLogPatterns := []struct {
+ pattern string
+ description string
+ }{
+ {
+ pattern: `azurerm_eventhub\s*\.\s*eventhub_for_activity_logs`,
+ description: "Activity Log Event Hub should be defined",
+ },
+ {
+ pattern: `azurerm_eventhub_namespace\s*\.\s*activity_logs_namespace`,
+ description: "Activity Log namespace should be defined",
+ },
+ {
+ pattern: `azurerm_eventhub_namespace_authorization_rule\s*\.\s*activity_logs_policy`,
+ description: "Activity Log authorization rule should be defined",
+ },
+ {
+ pattern: `azurerm_monitor_diagnostic_setting\s*\.\s*activity_logs_to_event_hub`,
+ description: "Activity Log diagnostic setting should be defined",
+ },
+ {
+ pattern: `sumologic_azure_event_hub_log_source\s*\.\s*sumo_activity_log_source`,
+ description: "Sumo Logic Activity Log source should be defined",
+ },
+ {
+ pattern: `name\s*=\s*"SumoActivityLogExport"`,
+ description: "Activity Log Event Hub should have correct name",
+ },
+ {
+ pattern: `azurerm_monitor_diagnostic_setting\s*\.\s*activity_logs_to_event_hub[\s\S]*?target_resource_id\s*=\s*"/subscriptions/[^/]+"\s*$`,
+ description: "Activity Log diagnostic setting should target subscription",
+ },
+ {
+ pattern: `category\s*=\s*"Administrative"`,
+ description: "Activity Log source should have Administrative category",
+ },
+ {
+ pattern: `enabled_log\s*\{[\s\S]*?category\s*=\s*"Administrative"`,
+ description: "Activity Log diagnostic setting should enable Administrative logs",
+ },
+ }
+
+ // Test base patterns
+ for _, pattern := range basePatterns {
+ matched, err := regexp.MatchString(pattern.pattern, planContent)
+ require.NoError(t, err, "Failed to compile regex pattern: %s", pattern.pattern)
+
+ if pattern.required {
+ assert.True(t, matched, pattern.description+". Pattern: %s", pattern.pattern)
+ if matched {
+ t.Logf("✓ %s", pattern.description)
+ }
+ } else {
+ if matched {
+ t.Logf("✓ %s", pattern.description)
+ } else {
+ t.Logf("- %s (not found - depends on Azure resource discovery)", pattern.description)
+ }
+ }
+ }
+
+ // Test Activity Log patterns based on configuration
+ for _, pattern := range activityLogPatterns {
+ matched, err := regexp.MatchString(pattern.pattern, planContent)
+ require.NoError(t, err, "Failed to compile regex pattern: %s", pattern.pattern)
+
+ if activityLogsEnabled {
+ // When Activity Logs are enabled, these patterns should be present
+ if matched {
+ t.Logf("✓ %s (Activity Logs enabled)", pattern.description)
+ } else {
+ t.Logf("- %s (expected when Activity Logs enabled but not found)", pattern.description)
+ }
+ } else {
+ // When Activity Logs are disabled, these patterns should NOT be present
+ assert.False(t, matched, "Activity Log pattern should not be present when disabled: %s", pattern.description)
+ if !matched {
+ t.Logf("✓ %s correctly absent (Activity Logs disabled)", pattern.description)
+ }
+ }
+ }
+
+ // Count resources in plan
+ planMatches := regexp.MustCompile(`Plan:\s+(\d+)\s+to\s+add`).FindStringSubmatch(planContent)
+ if len(planMatches) > 1 {
+ t.Logf("Terraform plan shows %s resources to add", planMatches[1])
+ }
+
+ // Count specific resource types
+ collectorMatches := regexp.MustCompile(`sumologic_collector\s*\.\s*sumo_collector`).FindAllString(planContent, -1)
+ activityLogSourceMatches := regexp.MustCompile(`sumologic_azure_event_hub_log_source\s*\.\s*sumo_activity_log_source`).FindAllString(planContent, -1)
+ eventHubMatches := regexp.MustCompile(`azurerm_eventhub\s*\.\s*eventhubs_by_type_and_location`).FindAllString(planContent, -1)
+ activityEventHubMatches := regexp.MustCompile(`azurerm_eventhub\s*\.\s*eventhub_for_activity_logs`).FindAllString(planContent, -1)
+
+ // Validate resource counts
+ assert.GreaterOrEqual(t, len(collectorMatches), 1, "Should have at least 1 Sumo Logic collector")
+
+ if activityLogsEnabled {
+ t.Logf("Activity Logs enabled - validating Activity Log resources")
+ if len(activityLogSourceMatches) > 0 {
+ t.Logf("✓ Found %d Activity Log sources", len(activityLogSourceMatches))
+ }
+ if len(activityEventHubMatches) > 0 {
+ t.Logf("✓ Found %d Activity Log Event Hubs", len(activityEventHubMatches))
+ }
+ } else {
+ t.Logf("Activity Logs disabled - validating absence of Activity Log resources")
+ assert.Equal(t, 0, len(activityLogSourceMatches), "Should have no Activity Log sources when disabled")
+ assert.Equal(t, 0, len(activityEventHubMatches), "Should have no Activity Log Event Hubs when disabled")
+ if len(activityLogSourceMatches) == 0 {
+ t.Logf("✓ No Activity Log sources found (correctly disabled)")
+ }
+ if len(activityEventHubMatches) == 0 {
+ t.Logf("✓ No Activity Log Event Hubs found (correctly disabled)")
+ }
+ }
+
+ if len(eventHubMatches) > 0 {
+ t.Logf("Found %d resource-specific Event Hubs", len(eventHubMatches))
+ } else {
+ t.Logf("No resource-specific Event Hubs found (expected in test environment without real Azure resources)")
+ }
+
+ t.Logf("Activity Log configuration validation completed. Activity Logs enabled: %v", activityLogsEnabled)
+}
+
+func TestSumoLogicAzureMetricsSourceConfiguration(t *testing.T) {
+ // Test Azure Metrics source configuration
+ terraformOptions := createTerraformOptions(filepath.Join("test", fixturesDir, "activity-logs-enabled.tfvars"))
+
+ terraform.Init(t, terraformOptions)
+ plan, err := terraform.PlanE(t, terraformOptions)
+
+ // We expect this might fail with API errors, but should not fail with validation errors
+ if err != nil {
+ errStr := err.Error()
+ assert.False(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule"),
+ "Should not have validation errors: %v", err)
+
+ t.Logf("Azure metrics source test passed validation but failed at runtime (expected): %v", err)
+
+ // Even with authentication failure, we can still validate the plan structure
+ if strings.Contains(errStr, "Terraform planned the following actions") {
+ t.Logf("Plan was generated before authentication failure, proceeding with validation")
+ validateAzureMetricsPlanContent(t, errStr)
+ }
+ return
+ }
+
+ // If no error, validate the successful plan
+ validateAzureMetricsPlanContent(t, plan)
+}
+
+// Helper function to validate Azure Metrics plan content
+func validateAzureMetricsPlanContent(t *testing.T, planContent string) {
+ // Define expected patterns for Azure Metrics configuration based on actual Terraform resource
+ expectedPatterns := []struct {
+ pattern string
+ description string
+ required bool
+ }{
+ {
+ pattern: `sumologic_azure_metrics_source\s*\.\s*terraform_azure_metrics_source`,
+ description: "Sumo Logic Azure Metrics source should be defined",
+ required: false, // Depends on Azure resource discovery
+ },
+ {
+ pattern: `content_type\s*=\s*"AzureMetrics"`,
+ description: "Metrics source should have correct content type",
+ required: false,
+ },
+ {
+ pattern: `type\s*=\s*"AzureClientSecretAuthentication"`,
+ description: "Metrics source should use client secret authentication",
+ required: false,
+ },
+ {
+ pattern: `tenant_id\s*=\s*"a39bedba-be8f-4c0f-bfe2-b8c7913501ea"`,
+ description: "Azure tenant ID should be configured for metrics source",
+ required: false,
+ },
+ {
+ pattern: `client_id\s*=\s*"a1e5fb4a-8644-4867-be4d-a54d0aeaaeed"`,
+ description: "Azure client ID should be configured for metrics source",
+ required: false,
+ },
+ {
+ pattern: `client_secret\s*=\s*\(sensitive value\)`,
+ description: "Azure client secret should be configured for metrics source",
+ required: false,
+ },
+ {
+ pattern: `type\s*=\s*"AzureMetricsPath"`,
+ description: "Metrics source should have Azure metrics path type",
+ required: false,
+ },
+ {
+ pattern: `environment\s*=\s*"Azure"`,
+ description: "Metrics source should target Azure environment",
+ required: false,
+ },
+ {
+ pattern: `namespace\s*=\s*"Microsoft\.KeyVault/vaults"`,
+ description: "Metrics source should target KeyVault namespace",
+ required: false,
+ },
+ {
+ pattern: `limit_to_namespaces\s*=\s*\[[\s\S]*?"logs"[\s\S]*?"metrics"[\s\S]*?\]`,
+ description: "Metrics source should limit to logs and metrics namespaces",
+ required: false,
+ },
+ {
+ pattern: `limit_to_regions\s*=\s*\[[\s\S]*?"eastus"[\s\S]*?"westus2"[\s\S]*?\]`,
+ description: "Metrics source should limit to specific regions",
+ required: false,
+ },
+ {
+ pattern: `name\s*=\s*"logs-collection-destination"`,
+ description: "Azure tag filters should include collection destination tag",
+ required: false,
+ },
+ {
+ pattern: `values\s*=\s*\[[\s\S]*?"sumologic"[\s\S]*?\]`,
+ description: "Azure tag filters should include sumologic value",
+ required: false,
+ },
+ }
+
+ // Validate collector (should always be present)
+ collectorPattern := `sumologic_collector\s*\.\s*sumo_collector`
+ collectorMatched, err := regexp.MatchString(collectorPattern, planContent)
+ require.NoError(t, err, "Failed to compile collector regex pattern")
+ assert.True(t, collectorMatched, "Sumo Logic collector should always be defined")
+ if collectorMatched {
+ t.Logf("✓ Sumo Logic collector is defined")
+ }
+
+ // Test Azure Metrics patterns
+ metricsSourceFound := false
+ for _, expected := range expectedPatterns {
+ matched, err := regexp.MatchString(expected.pattern, planContent)
+ require.NoError(t, err, "Failed to compile regex pattern: %s", expected.pattern)
+
+ if matched {
+ t.Logf("✓ %s", expected.description)
+ if strings.Contains(expected.pattern, "terraform_azure_metrics_source") {
+ metricsSourceFound = true
+ }
+ } else {
+ if expected.required {
+ assert.True(t, matched, expected.description+". Pattern: %s", expected.pattern)
+ } else {
+ t.Logf("- %s (not found - might depend on Azure resource discovery)", expected.description)
+ }
+ }
+ }
+
+ // Count metrics sources in plan
+ metricsMatches := regexp.MustCompile(`sumologic_azure_metrics_source\s*\.\s*terraform_azure_metrics_source`).FindAllString(planContent, -1)
+
+ if len(metricsMatches) > 0 {
+ t.Logf("Found %d Azure Metrics sources", len(metricsMatches))
+ assert.True(t, metricsSourceFound, "Should have Azure Metrics source when metrics resources exist")
+ } else {
+ t.Logf("No Azure Metrics sources found (expected in environments without discovered Azure resources)")
+ }
+
+ // Validate that we're NOT looking for incorrect diagnostic categories
+ diagnosticErrors := []string{
+ "microsoft.keyvault/vaults/metrics",
+ "microsoft.keyvault/vaults/logs",
+ }
+
+ for _, errorPattern := range diagnosticErrors {
+ if strings.Contains(planContent, errorPattern) {
+ t.Logf("⚠️ Found reference to invalid diagnostic category: %s", errorPattern)
+ t.Logf(" Azure Metrics source should use 'Microsoft.KeyVault/vaults' namespace, not diagnostic categories")
+ }
+ }
+
+ t.Logf("Azure Metrics Source configuration validation completed")
+}
+
+// Test Sumo Logic source naming conventions (no Terraform execution required)
+func TestSumoLogicSourceNamingConventions(t *testing.T) {
+ testCases := []struct {
+ name string
+ subscriptionID string
+ collectorName string
+ expectedSourceName string
+ description string
+ }{
+ {
+ name: "StandardNaming",
+ subscriptionID: "c088dc46-d692-42ad-a4b6-e02d56cc5596",
+ collectorName: "Azure-Test-Collector",
+ expectedSourceName: "Azure-Test-Collector-c088dc46-d692-42ad-a4b6-e02d56cc5596",
+ description: "Standard naming should combine collector name with subscription ID",
+ },
+ {
+ name: "CollectorWithDashes",
+ subscriptionID: "c088dc46-d692-42ad-a4b6-e02d56cc5596",
+ collectorName: "Test-Collector-Name",
+ expectedSourceName: "Test-Collector-Name-c088dc46-d692-42ad-a4b6-e02d56cc5596",
+ description: "Collector names with dashes should work correctly",
+ },
+ {
+ name: "CollectorWithUnderscores",
+ subscriptionID: "c088dc46-d692-42ad-a4b6-e02d56cc5596",
+ collectorName: "Test_Collector_Name",
+ expectedSourceName: "Test_Collector_Name-c088dc46-d692-42ad-a4b6-e02d56cc5596",
+ description: "Collector names with underscores should work correctly",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Apply the same naming logic as in Terraform configuration
+ constructedName := fmt.Sprintf("%s-%s", tc.collectorName, tc.subscriptionID)
+
+ assert.Equal(t, tc.expectedSourceName, constructedName,
+ fmt.Sprintf("Source name construction should match expected: %s", tc.expectedSourceName))
+
+ assert.Contains(t, constructedName, tc.collectorName,
+ "Final source name should contain collector name")
+ assert.Contains(t, constructedName, tc.subscriptionID,
+ "Final source name should contain subscription ID")
+ })
+ }
+}
+
+// Test Sumo Logic app validation patterns (no Terraform execution required)
+func TestSumoLogicAppValidationPatterns(t *testing.T) {
+ testCases := []struct {
+ name string
+ appUUID string
+ appName string
+ appVersion string
+ shouldPass bool
+ description string
+ }{
+ {
+ name: "ValidAzureStorageApp",
+ appUUID: "53376d23-2687-4500-b61e-4a2e2a119658",
+ appName: "Azure Storage",
+ appVersion: "1.0.3",
+ shouldPass: true,
+ description: "Valid Azure Storage app should pass validation",
+ },
+ {
+ name: "ValidAzureKeyVaultApp",
+ appUUID: "b20abced-0122-4c7a-8833-c68c3c29c3d3",
+ appName: "Azure Key Vault",
+ appVersion: "1.0.2",
+ shouldPass: true,
+ description: "Valid Azure Key Vault app should pass validation",
+ },
+ {
+ name: "ValidLatestVersion",
+ appUUID: "53376d23-2687-4500-b61e-4a2e2a119658",
+ appName: "Test App",
+ appVersion: "latest",
+ shouldPass: true,
+ description: "Version 'latest' should pass validation",
+ },
+ {
+ name: "InvalidUUIDFormat",
+ appUUID: "invalid-uuid",
+ appName: "Test App",
+ appVersion: "1.0.0",
+ shouldPass: false,
+ description: "Invalid UUID format should fail validation",
+ },
+ {
+ name: "InvalidVersionFormat",
+ appUUID: "53376d23-2687-4500-b61e-4a2e2a119658",
+ appName: "Test App",
+ appVersion: "invalid",
+ shouldPass: false,
+ description: "Invalid version format should fail validation",
+ },
+ {
+ name: "EmptyFields",
+ appUUID: "",
+ appName: "",
+ appVersion: "",
+ shouldPass: false,
+ description: "Empty fields should fail validation",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Test UUID format validation
+ uuidPattern := `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`
+ uuidMatched, err := regexp.MatchString(uuidPattern, tc.appUUID)
+ require.NoError(t, err)
+
+ // Test semantic version pattern or "latest"
+ versionPattern := `^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$`
+ versionMatched := tc.appVersion == "latest" || func() bool {
+ matched, err := regexp.MatchString(versionPattern, tc.appVersion)
+ require.NoError(t, err)
+ return matched
+ }()
+
+ // Test name is not empty
+ nameValid := strings.TrimSpace(tc.appName) != ""
+
+ allValid := uuidMatched && versionMatched && nameValid
+
+ if tc.shouldPass {
+ assert.True(t, allValid, tc.description)
+ if allValid {
+ t.Logf("✓ %s: UUID=%s, Name=%s, Version=%s", tc.name, tc.appUUID, tc.appName, tc.appVersion)
+ }
+ } else {
+ assert.False(t, allValid, tc.description)
+ if !allValid {
+ t.Logf("✓ %s correctly failed validation", tc.name)
+ }
+ }
+ })
+ }
+}
+
+// TestSumoLogicAppsInstallationPlanValidation tests installation_apps_list validation - HIGH PRIORITY MISSING
+func TestSumoLogicAppsInstallationPlanValidation(t *testing.T) {
+ // Simple test using the existing helper patterns from azure_test.go
+ terraformOptions := createTerraformOptions(filepath.Join("test", fixturesDir, "sumo-valid-apps.tfvars"))
+
+ terraform.Init(t, terraformOptions)
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ // We expect this might fail with API errors, but should not fail with validation errors
+ if err != nil {
+ errStr := err.Error()
+ assert.False(t,
+ strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule"),
+ "Should not have validation errors for valid apps: %v", err)
+
+ t.Logf("Valid apps test passed validation but failed at runtime (expected for test environment): %v", err)
+ } else {
+ t.Logf("✓ Valid Sumo Logic apps configuration passed validation and plan generation")
+ }
+}
+
+// TestSumoLogicCollectorNameValidation tests comprehensive collector name validation - HIGH PRIORITY MISSING
+func TestSumoLogicCollectorNameValidation(t *testing.T) {
+ testCases := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidCollectorWithDashes",
+ tfvarsFile: "test/fixtures/sumo-collector-dashes.tfvars",
+ expectError: false,
+ description: "Collector name with dashes should work",
+ },
+ {
+ name: "CollectorNameWithSpecialChars",
+ tfvarsFile: "test/fixtures/sumo-invalid-collector-special.tfvars",
+ expectError: true,
+ description: "Collector name with special characters should fail validation",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tc.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+ _, err := terraform.PlanE(t, terraformOptions)
+
+ if tc.expectError {
+ assert.Error(t, err, tc.description)
+ // Verify it's a validation error, not an API error
+ if err != nil {
+ errStr := err.Error()
+ isValidationError := strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") ||
+ strings.Contains(errStr, "error_message")
+
+ if isValidationError {
+ t.Logf("✓ %s correctly failed with validation error: %s", tc.name, tc.description)
+ } else {
+ t.Logf("⚠️ %s failed but not with validation error (might be API error): %v", tc.name, err)
+ }
+ }
+ } else {
+ // For valid configurations, we might get API errors trying to access resources
+ // We only care that validation passed
+ if err != nil {
+ errStr := err.Error()
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tc.name, err)
+ } else {
+ // API errors are expected for validation-only tests
+ t.Logf("✓ %s passed validation but failed at runtime (expected): %s", tc.name, tc.description)
+ }
+ } else {
+ t.Logf("✓ %s passed validation completely: %s", tc.name, tc.description)
+ }
+ }
+ })
+ }
+}
+
+// TestSumoLogicLogSourceFilters tests log source filters (Include, Exclude, Mask)
+// Validates that filters are applied correctly to Sumo Logic Event Hub log sources
+func TestSumoLogicLogSourceFilters(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidLogSourceFilters",
+ tfvarsFile: filepath.Join("test", fixturesDir, "log-source-filters-valid.tfvars"),
+ expectError: false,
+ description: "Valid log source filters with Include and Mask should pass validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tt.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+ planOutput, err := terraform.PlanE(t, terraformOptions)
+
+ if tt.expectError {
+ assert.Error(t, err, tt.description)
+ } else {
+ // For valid configurations, validation should pass
+ if err != nil {
+ errStr := err.Error()
+ // Check for validation errors
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tt.name, err)
+ } else {
+ // API/runtime errors are expected
+ t.Logf("✓ Test case '%s' passed validation but failed at runtime (expected): %v", tt.name, err)
+
+ // Check if plan was generated before error
+ if strings.Contains(errStr, "Terraform planned the following actions") ||
+ strings.Contains(errStr, "filters") {
+ t.Logf("✓ Filters configuration found in plan output")
+ }
+ }
+ } else {
+ // Validate that filters are in the plan
+ if strings.Contains(planOutput, "filters {") &&
+ strings.Contains(planOutput, "filter_type") {
+ t.Logf("✅ Test case '%s' passed: log source filters configured", tt.name)
+ } else {
+ t.Logf("✅ Test case '%s' passed validation", tt.name)
+ }
+ }
+ }
+ })
+ }
+}
+
+// TestSumoLogicActivityLogFilters tests activity log filters
+// Validates that filters are applied correctly to activity log sources
+func TestSumoLogicActivityLogFilters(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidActivityLogFilters",
+ tfvarsFile: filepath.Join("test", fixturesDir, "activity-log-filters-valid.tfvars"),
+ expectError: false,
+ description: "Valid activity log filters should pass validation",
+ },
+ {
+ name: "InvalidMaskWithColons",
+ tfvarsFile: filepath.Join("test", fixturesDir, "activity-log-filters-invalid-mask.tfvars"),
+ expectError: true,
+ description: "Mask filter with colons should fail Sumo Logic API validation",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tt.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+ planOutput, err := terraform.PlanE(t, terraformOptions)
+
+ if tt.expectError {
+ // Should get API error about colons in mask string
+ assert.Error(t, err, tt.description)
+ if err != nil {
+ errStr := err.Error()
+ if strings.Contains(errStr, "mask string should not contain any colons") {
+ t.Logf("✓ Test case '%s' correctly failed with expected error: mask contains colons", tt.name)
+ } else {
+ t.Logf("⚠️ Test case '%s' failed but error might be different: %v", tt.name, err)
+ }
+ }
+ } else {
+ // For valid configurations, validation should pass
+ if err != nil {
+ errStr := err.Error()
+ // Check for validation errors
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tt.name, err)
+ } else {
+ // API/runtime errors are expected
+ t.Logf("✓ Test case '%s' passed validation but failed at runtime (expected): %v", tt.name, err)
+
+ // Check if plan contains filter configuration
+ if strings.Contains(errStr, "filters {") ||
+ strings.Contains(errStr, "filter_type") {
+ t.Logf("✓ Activity log filters configuration found")
+ }
+ }
+ } else {
+ // Validate that filters are in the plan
+ if strings.Contains(planOutput, "sumo_activity_log_source") &&
+ strings.Contains(planOutput, "filters {") {
+ t.Logf("✅ Test case '%s' passed: activity log filters configured", tt.name)
+ } else {
+ t.Logf("✅ Test case '%s' passed validation", tt.name)
+ }
+ }
+ }
+ })
+ }
+}
+
+// TestSumoLogicMetricsSourceTagFiltering tests per-resource-type required_resource_tags for metrics sources
+// Validates that tag filtering is applied correctly to Sumo Logic metrics sources
+func TestSumoLogicMetricsSourceTagFiltering(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "NoRequiredResourceTags",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-omitted.tfvars"),
+ expectError: false,
+ description: "Omitted required_resource_tags should collect metrics from all Azure resources",
+ },
+ {
+ name: "EmptyRequiredResourceTags",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-empty.tfvars"),
+ expectError: false,
+ description: "Empty required_resource_tags = {} should collect metrics from all Azure resources",
+ },
+ {
+ name: "SingleTagFilter",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-single.tfvars"),
+ expectError: false,
+ description: "Single tag should filter Azure resources for metrics collection",
+ },
+ {
+ name: "MultipleTagsFilter",
+ tfvarsFile: filepath.Join("test", fixturesDir, "tag-filter-multiple.tfvars"),
+ expectError: false,
+ description: "Multiple tags should use AND logic to filter Azure resources for metrics collection",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tt.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+
+ planOutput, err := terraform.PlanE(t, terraformOptions)
+
+ if tt.expectError {
+ assert.Error(t, err, tt.description)
+ } else {
+ // For valid configurations, validation should pass
+ if err != nil {
+ errStr := err.Error()
+ // Check for validation errors - these would indicate a problem
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tt.name, err)
+ } else {
+ // API/runtime errors are expected for validation-only tests
+ t.Logf("✓ Test case '%s' passed validation but failed at runtime (expected): %v", tt.name, err)
+ }
+ } else {
+ // Validate that metrics source is configured in the plan
+ if strings.Contains(planOutput, "sumologic_azure_metrics_source") {
+ t.Logf("✅ Test case '%s' passed: metrics source configured", tt.name)
+ } else {
+ t.Logf("✅ Test case '%s' passed validation", tt.name)
+ }
+ }
+ }
+ })
+ }
+}
+
+// TestSumoLogicMetricsSourceFilters tests metrics source filters (Include, Exclude, Mask)
+// Validates that filters are applied correctly to Sumo Logic Azure metrics sources
+func TestSumoLogicMetricsSourceFilters(t *testing.T) {
+ tests := []struct {
+ name string
+ tfvarsFile string
+ expectError bool
+ description string
+ }{
+ {
+ name: "ValidMetricsSourceFilters",
+ tfvarsFile: filepath.Join("test", fixturesDir, "metrics-source-filters-valid.tfvars"),
+ expectError: false,
+ description: "Valid metrics source filters with Include and Mask should pass validation",
+ },
+ {
+ name: "MultipleMetricsFilters",
+ tfvarsFile: filepath.Join("test", fixturesDir, "metrics-source-filters-multiple.tfvars"),
+ expectError: false,
+ description: "Multiple metrics filters (Include, Exclude, Mask) should all be configured correctly",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ terraformOptions := createTerraformOptions(tt.tfvarsFile)
+
+ terraform.Init(t, terraformOptions)
+ planOutput, err := terraform.PlanE(t, terraformOptions)
+
+ // For valid configurations, validation should pass
+ if err != nil {
+ errStr := err.Error()
+ // Check for validation errors
+ if strings.Contains(errStr, "Invalid value for variable") ||
+ strings.Contains(errStr, "validation rule") {
+ t.Errorf("Test case '%s' should pass validation but got validation error: %v", tt.name, err)
+ } else {
+ // API/runtime errors are expected
+ t.Logf("✓ Test case '%s' passed validation but failed at runtime (expected): %v", tt.name, err)
+
+ // Check if plan was generated before error
+ if strings.Contains(errStr, "Terraform planned the following actions") ||
+ strings.Contains(errStr, "filters") {
+ t.Logf("✓ Metrics filters configuration found in plan output")
+ }
+ }
+ } else {
+ // Validate that filters are in the plan
+ if strings.Contains(planOutput, "filters {") &&
+ strings.Contains(planOutput, "filter_type") {
+ t.Logf("✅ Test case '%s' passed: metrics source filters configured", tt.name)
+ } else {
+ t.Logf("✅ Test case '%s' passed validation", tt.name)
+ }
+ }
+ })
+ }
+}
+func TestScanIntervalValidation(t *testing.T) {
+ testCases := []struct {
+ name string
+ tfvarsFile string
+ shouldPass bool
+ description string
+ }{
+ {
+ name: "ValidScanIntervalMinimum",
+ tfvarsFile: filepath.Join("test", fixturesDir, "scan-interval-valid.tfvars"),
+ shouldPass: true,
+ description: "Minimum scan_interval value (1000ms) should pass validation",
+ },
+ {
+ name: "InvalidScanIntervalBelowMin",
+ tfvarsFile: filepath.Join("test", fixturesDir, "scan-interval-invalid-below-min.tfvars"),
+ shouldPass: false,
+ description: "scan_interval below minimum (999ms) should fail validation",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ runValidationTest(t, tc.name, tc.tfvarsFile, !tc.shouldPass, tc.description)
+ })
+ }
+}
+
+func TestSumoLogicEnvironmentBaseURLValidation(t *testing.T) {
+ testCases := []struct {
+ name string
+ tfvarsFile string
+ shouldPass bool
+ description string
+ }{
+ {
+ name: "ValidBaseURL",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-base-url-valid.tfvars"),
+ shouldPass: true,
+ description: "Valid base URL for Switzerland deployment should pass",
+ },
+ {
+ name: "InvalidBaseURLFormat",
+ tfvarsFile: filepath.Join("test", fixturesDir, "sumo-base-url-invalid-protocol.tfvars"),
+ shouldPass: false,
+ description: "Base URL with invalid format should fail validation",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ runValidationTest(t, tc.name, tc.tfvarsFile, !tc.shouldPass, tc.description)
+ })
+ }
+}
\ No newline at end of file
diff --git a/azure-collection-terraform/variables.tf b/azure-collection-terraform/variables.tf
new file mode 100644
index 00000000..8b10cfdc
--- /dev/null
+++ b/azure-collection-terraform/variables.tf
@@ -0,0 +1,518 @@
+variable "azure_subscription_id" {
+ description = "The subscription id where your azure resources are present. If not provided, will use the current Azure CLI context."
+ type = string
+ default = null
+
+ validation {
+ condition = var.azure_subscription_id == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_subscription_id))
+ error_message = "The azure_subscription_id must be a valid UUID format or null for Azure CLI auto-detection."
+ }
+}
+
+variable "azure_client_id" {
+ description = "The client id for Azure authentication. If not provided, will use Azure CLI context."
+ type = string
+ default = null
+
+ validation {
+ condition = var.azure_client_id == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_client_id))
+ error_message = "The azure_client_id must be a valid UUID format or null."
+ }
+}
+
+variable "azure_client_secret" {
+ description = "The client secret for Azure authentication. If not provided, will use Azure CLI context."
+ type = string
+ default = null
+ sensitive = true
+
+ validation {
+ condition = var.azure_client_secret == null || try(length(var.azure_client_secret) > 0, false)
+ error_message = "The azure_client_secret must be a non-empty string or null."
+ }
+}
+
+variable "azure_tenant_id" {
+ description = "The Tenant Id. If not provided, will use the current Azure CLI context."
+ type = string
+ default = null
+
+ validation {
+ condition = var.azure_tenant_id == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.azure_tenant_id))
+ error_message = "The azure_tenant_id must be a valid UUID format or null."
+ }
+}
+
+variable "target_resource_types" {
+ type = list(object({
+ log_namespace = optional(string)
+ metric_namespace = optional(string)
+ log_categories = optional(list(string), [])
+ required_resource_tags = optional(map(string), {})
+ name_filter = optional(string, "")
+ log_source_filters = optional(list(object({
+ filter_type = string
+ name = string
+ regexp = string
+ mask = optional(string, null)
+ regions = optional(list(string), [])
+ })), [])
+ metrics_source_filters = optional(list(object({
+ filter_type = string
+ name = string
+ regexp = string
+ mask = optional(string, null)
+ })), [])
+ }))
+ description = "List of Azure resource types with their log and metric namespace configuration. Both namespace fields are optional, but at least one must be provided. The required_resource_tags field filters resources for this specific type using AND logic (all tags must match). The name_filter field is optional and filters resources by regex pattern (case-insensitive); if omitted or empty string, no name filtering is applied. The log_source_filters field is optional and specifies filters to apply to the Sumo Logic Azure Event Hub log source for this resource type. Each filter can optionally specify regions (list of region names); if omitted or empty, the filter applies to all regions. The metrics_source_filters field is optional and specifies filters to apply to the Sumo Logic Azure metrics source for this resource type."
+
+ validation {
+ condition = alltrue([
+ for resource_config in var.target_resource_types :
+ resource_config.log_namespace == null || resource_config.log_namespace == "" || can(regex("^Microsoft\\.[A-Za-z0-9]+/[A-Za-z0-9/]+$", resource_config.log_namespace))
+ ])
+ error_message = "All log_namespace values must be valid Azure resource types in the format 'Microsoft.Service/resourceType' (e.g., 'Microsoft.KeyVault/vaults', 'Microsoft.Storage/storageAccounts') or null/empty."
+ }
+
+ validation {
+ condition = alltrue([
+ for resource_config in var.target_resource_types :
+ resource_config.metric_namespace == null || resource_config.metric_namespace == "" || can(regex("^Microsoft\\.[A-Za-z0-9]+/[A-Za-z0-9/]+$", resource_config.metric_namespace))
+ ])
+ error_message = "All metric_namespace values must be valid Azure resource types in the format 'Microsoft.Service/resourceType' (e.g., 'Microsoft.KeyVault/vaults', 'Microsoft.Storage/storageAccounts') or null/empty."
+ }
+
+ validation {
+ condition = alltrue([
+ for resource_config in var.target_resource_types :
+ (resource_config.log_namespace == null || resource_config.log_namespace == "" ? 0 : length(resource_config.log_namespace)) <= 200 &&
+ (resource_config.metric_namespace == null || resource_config.metric_namespace == "" ? 0 : length(resource_config.metric_namespace)) <= 200
+ ])
+ error_message = "Resource type namespaces must be 200 characters or less."
+ }
+
+ validation {
+ condition = length(var.target_resource_types) >= 0
+ error_message = "Target resource types must be a valid list."
+ }
+
+ validation {
+ condition = alltrue([
+ for resource_config in var.target_resource_types :
+ (resource_config.log_namespace != null && resource_config.log_namespace != "") ||
+ (resource_config.metric_namespace != null && resource_config.metric_namespace != "")
+ ])
+ error_message = "Each resource type must have at least one namespace defined (log_namespace or metric_namespace cannot both be null/empty)."
+ }
+
+ validation {
+ condition = length(distinct([
+ for config in var.target_resource_types :
+ config.log_namespace
+ if config.log_namespace != null && config.log_namespace != ""
+ ])) == length([
+ for config in var.target_resource_types :
+ config.log_namespace
+ if config.log_namespace != null && config.log_namespace != ""
+ ])
+ error_message = "Duplicate log_namespace values are not allowed."
+ }
+
+
+}
+
+variable "required_resource_tags" {
+ description = "A map of tags to filter Azure resources by."
+ type = map(string)
+}
+
+variable "nested_namespace_configs" {
+ description = "Map of parent resource types to their child resource types that should be monitored"
+ type = map(list(string))
+}
+
+variable "resource_group_name" {
+ description = "The name of the Resource Group."
+ type = string
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9._-]+$", var.resource_group_name))
+ error_message = "Resource group name can only contain alphanumeric characters, periods (.), underscores (_), and hyphens (-). Spaces are not allowed."
+ }
+
+ validation {
+ condition = length(var.resource_group_name) >= 1 && length(var.resource_group_name) <= 90
+ error_message = "Resource group name must be between 1 and 90 characters long."
+ }
+
+ validation {
+ condition = !can(regex("^[._-]|[._-]$", var.resource_group_name))
+ error_message = "Resource group name should not start or end with periods, underscores, or hyphens."
+ }
+
+ validation {
+ condition = !contains(["microsoft", "azure", "system"], lower(var.resource_group_name))
+ error_message = "Resource group name cannot use reserved names like 'Microsoft', 'Azure', or 'System'."
+ }
+}
+
+variable "eventhub_namespace_name" {
+ description = "The name of the Event Hub Namespace."
+ type = string
+
+ validation {
+ condition = length(var.eventhub_namespace_name) >= 6 && length(var.eventhub_namespace_name) <= 50
+ error_message = "Event Hub namespace name must be between 6 and 50 characters long."
+ }
+
+ validation {
+ condition = can(regex("^[a-zA-Z]", var.eventhub_namespace_name))
+ error_message = "Event Hub namespace name must begin with a letter (a-z or A-Z)."
+ }
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9-]+$", var.eventhub_namespace_name))
+ error_message = "Event Hub namespace name can only contain letters (a-z, A-Z), numbers (0-9), and hyphens (-)."
+ }
+
+ validation {
+ condition = can(regex("[a-zA-Z0-9]$", var.eventhub_namespace_name))
+ error_message = "Event Hub namespace name must end with a letter or a number."
+ }
+
+ default = "SUMO-267667-Hub"
+}
+
+variable "location" {
+ description = "The location for the resources."
+ type = string
+
+ validation {
+ condition = contains([
+ "East US", "East US 2", "West US", "West US 2", "West US 3", "Central US", "North Central US", "South Central US", "West Central US",
+ "Canada Central", "Canada East",
+ "Brazil South", "Brazil Southeast",
+ "North Europe", "West Europe", "UK South", "UK West", "France Central", "France South", "Germany West Central", "Germany North",
+ "Switzerland North", "Switzerland West", "Norway East", "Norway West",
+ "East Asia", "Southeast Asia", "Japan East", "Japan West", "Australia East", "Australia Southeast", "Australia Central", "Australia Central 2",
+ "Korea Central", "Korea South", "India Central", "India South", "India West",
+ "UAE Central", "UAE North", "South Africa North", "South Africa West"
+ ], var.location)
+ error_message = "Location must be a valid Azure region. Common regions include: East US, West US 2, Central US, North Europe, West Europe, Southeast Asia, etc."
+ }
+}
+
+# Throughput units (applies to Standard and Premium SKUs only)
+variable "default_throughput_units" {
+ description = "The number of throughput units to assign for Event Hub namespaces (global default). Only applicable for Standard and Premium SKUs. Valid values: 1, 2, 4, 8, or 16."
+ type = number
+ default = 4
+
+ validation {
+ condition = contains([1, 2, 4, 8, 16], var.default_throughput_units)
+ error_message = "Throughput units must be one of: 1, 2, 4, 8, or 16."
+ }
+
+ validation {
+ condition = floor(var.default_throughput_units) == var.default_throughput_units
+ error_message = "Throughput units must be a whole number."
+ }
+}
+
+variable "eventhub_namespace_sku" {
+ description = <<-EOT
+ The SKU (pricing tier) of the Event Hub Namespace (global default). Valid values: Basic, Standard, Premium, or Dedicated.
+
+ IMPORTANT: Auto-Upgrade Behavior
+ If a region has more than 10 Event Hub instances and the SKU is Basic or Standard, the system will automatically
+ upgrade to Premium SKU for that region. This is necessary because:
+ - Basic/Standard SKUs support max 10 Event Hubs per namespace
+ - Premium SKU supports up to 100 Event Hubs per namespace
+
+ You can override this auto-upgrade behavior using region_specific_eventhub_skus.
+ EOT
+ type = string
+
+ validation {
+ condition = contains(["Basic", "Standard", "Premium", "Dedicated"], var.eventhub_namespace_sku)
+ error_message = "Event Hub namespace SKU must be one of: Basic, Standard, Premium, or Dedicated."
+ }
+}
+
+variable "region_specific_eventhub_skus" {
+ description = <<-EOT
+ Optional map to override Event Hub SKU and throughput units for specific regions.
+ Keys are region names (case-insensitive, spaces ignored). Each value specifies:
+ - sku: "Basic", "Standard", "Premium", or "Dedicated" (overrides global eventhub_namespace_sku)
+ - throughput_units: 1, 2, 4, 8, or 16 (for Standard/Premium; ignored for Basic/Dedicated)
+
+ Example:
+ {
+ "East US" = {
+ sku = "Premium"
+ throughput_units = 8
+ }
+ "West Europe" = {
+ sku = "Standard"
+ throughput_units = 2
+ }
+ "Central US" = {
+ sku = "Basic"
+ throughput_units = 1
+ }
+ }
+ EOT
+ type = map(object({
+ sku = string
+ throughput_units = number
+ }))
+ default = {}
+
+ validation {
+ condition = alltrue([
+ for region, config in var.region_specific_eventhub_skus :
+ contains(["Basic", "Standard", "Premium", "Dedicated"], config.sku)
+ ])
+ error_message = "All region-specific SKUs must be one of: Basic, Standard, Premium, or Dedicated."
+ }
+
+ validation {
+ condition = alltrue([
+ for region, config in var.region_specific_eventhub_skus :
+ contains([1, 2, 4, 8, 16], config.throughput_units)
+ ])
+ error_message = "All region-specific throughput units must be one of: 1, 2, 4, 8, or 16."
+ }
+
+ validation {
+ condition = alltrue([
+ for region, config in var.region_specific_eventhub_skus :
+ floor(config.throughput_units) == config.throughput_units
+ ])
+ error_message = "All region-specific throughput units must be whole numbers."
+ }
+}
+
+variable "eventhub_namespace_unsupported_locations" {
+ description = "List of locations (regions) where Event Hub Namespace creation is not supported or should be skipped. Values can be human-friendly region names; comparison is case-insensitive and ignores spaces. Example: [\"China East\", \"US Gov Virginia\"]"
+ type = list(string)
+ default = [
+ "South Africa West",
+ "Australia Central 2",
+ "USGov Arizona",
+ "USGov Texas",
+ "USGov Virginia",
+ "Brazil Southeast",
+ "China East",
+ "China East 2",
+ "China East 3",
+ "China North",
+ "China North 2",
+ "China North 3",
+ "France South",
+ "Germany North",
+ "Norway West",
+ "Sweden South",
+ "Switzerland West",
+ "Taiwan North",
+ "UAE Central",
+ ]
+}
+
+variable "eventhub_namespace_limited_sku_locations" {
+ description = "List of locations (regions) that only support Basic & Standard SKUs for Event Hub namespaces. Values are human-friendly names; comparison is case-insensitive and ignores spaces. Example: [\"West India\", \"Mexico Central\"]"
+ type = list(string)
+ default = [
+ "West India",
+ "Mexico Central",
+ ]
+}
+
+variable "policy_name" {
+ description = "The name of the Shared Access Policy."
+ type = string
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9-]+$", var.policy_name))
+ error_message = "Policy name can only contain alphanumeric characters and hyphens, following Azure naming conventions."
+ }
+
+ validation {
+ condition = length(var.policy_name) >= 1 && length(var.policy_name) <= 64
+ error_message = "Policy name must be between 1 and 64 characters long."
+ }
+}
+
+variable "activity_log_export_name" {
+ type = string
+ description = "Activity Log Export Name"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9]$", var.activity_log_export_name)) || can(regex("^[a-zA-Z0-9]$", var.activity_log_export_name))
+ error_message = "Activity log export name must start and end with alphanumeric characters, and can contain letters, numbers, underscores, periods, and hyphens."
+ }
+
+ validation {
+ condition = length(var.activity_log_export_name) >= 1 && length(var.activity_log_export_name) <= 128
+ error_message = "Activity log export name must be between 1 and 128 characters long."
+ }
+
+ validation {
+ condition = !can(regex("__", var.activity_log_export_name)) && !can(regex("\\.\\.", var.activity_log_export_name)) && !can(regex("--", var.activity_log_export_name))
+ error_message = "Activity log export name cannot contain consecutive underscores, periods, or hyphens."
+ }
+}
+
+variable "activity_log_export_category" {
+ type = string
+ description = "Activity Log Export Category"
+}
+
+variable "enable_activity_logs" {
+ description = "Set to true to enable subscription-level activity log export."
+ type = bool
+}
+
+variable "activity_log_filters" {
+ type = list(object({
+ filter_type = string
+ name = string
+ regexp = string
+ mask = optional(string, null)
+ }))
+ default = []
+ description = "Optional filters to apply to the Sumo Logic Activity Log source. Each filter can Include, Exclude, or Mask log messages based on regex patterns. The mask field is required only for Mask filter type."
+}
+
+variable "sumologic_environment" {
+ type = string
+ description = "Enter au, ca, de, eu, jp, us2, kr, fed ch or us1. For more information on Sumo Logic deployments visit https://help.sumologic.com/APIs/General-API-Information/Sumo-Logic-Endpoints-and-Firewall-Security"
+
+ validation {
+ condition = contains([
+ "stag",
+ "long",
+ "au",
+ "ca",
+ "de",
+ "eu",
+ "jp",
+ "us1",
+ "us2",
+ "kr",
+ "fed",
+ "ch"
+ ], var.sumologic_environment)
+ error_message = "The value must be one of au, ca, de, eu, jp, us1, us2, kr, ch or fed."
+ }
+}
+
+variable "sumologic_environment_base_url" {
+ type = string
+ description = "Base URL for custom Sumo Logic environments (e.g., 'https://api.ch.sumologic.com/api/' for Switzerland). If provided, this takes precedence over the sumologic_environment parameter. Leave empty for standard deployments."
+ default = null
+
+ validation {
+ condition = var.sumologic_environment_base_url == null || can(regex("^https://[a-zA-Z0-9.-]+\\.sumologic\\.com/api/?$", var.sumologic_environment_base_url))
+ error_message = "The base URL must be null or a valid Sumo Logic API endpoint URL (e.g., 'https://api.ch.sumologic.com/api/')."
+ }
+}
+
+variable "sumologic_access_id" {
+ type = string
+ description = "Sumo Logic Access ID. Visit https://help.sumologic.com/Manage/Security/Access-Keys#Create_an_access_key"
+
+ validation {
+ condition = can(regex("\\w+", var.sumologic_access_id))
+ error_message = "The SumoLogic access ID must contain valid characters."
+ }
+}
+
+variable "sumologic_access_key" {
+ type = string
+ description = "Sumo Logic Access Key. Visit https://help.sumologic.com/Manage/Security/Access-Keys#Create_an_access_key"
+ sensitive = true
+
+ validation {
+ condition = can(regex("\\w+", var.sumologic_access_key))
+ error_message = "The SumoLogic access key must contain valid characters."
+ }
+}
+
+variable "installation_apps_list" {
+ description = "List of Sumo Logic apps to be installed. Each app can have custom parameters specific to that app."
+ type = list(object({
+ uuid = string
+ name = string
+ version = string
+ parameters = optional(map(string), {})
+ }))
+ default = []
+
+ validation {
+ condition = length(var.installation_apps_list) == 0 || alltrue([
+ for app in var.installation_apps_list :
+ can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", app.uuid))
+ ])
+ error_message = "All UUIDs must be in valid UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)."
+ }
+
+ validation {
+ condition = length(var.installation_apps_list) == 0 || alltrue([
+ for app in var.installation_apps_list :
+ length(app.name) > 0 && length(app.name) <= 100
+ ])
+ error_message = "App names must not be empty and must be 100 characters or less."
+ }
+
+ validation {
+ condition = length(var.installation_apps_list) == 0 || alltrue([
+ for app in var.installation_apps_list :
+ app.version == "latest" || can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", app.version))
+ ])
+ error_message = "App versions must be either 'latest' or in semantic version format (x.y.z)."
+ }
+
+ validation {
+ condition = length(var.installation_apps_list) == 0 || alltrue([
+ for app in var.installation_apps_list :
+ alltrue([
+ for key, value in app.parameters :
+ length(key) > 0 && length(key) <= 128 && length(value) <= 1024
+ ])
+ ])
+ error_message = "Parameter keys must be between 1-128 characters and values must be 1024 characters or less."
+ }
+}
+
+variable "sumo_collector_name" {
+ type = string
+ description = "Name for the SumoLogic collector"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9_-]+$", var.sumo_collector_name)) && length(var.sumo_collector_name) > 0 && length(var.sumo_collector_name) <= 128
+ error_message = "Collector name contains invalid characters. Please use alphanumeric characters, hyphens (-), and underscores (_) only."
+ }
+}
+
+variable "prevent_deletion_if_contains_resources" {
+ description = <<-EOT
+ Controls the azurerm provider's resource_group.prevent_deletion_if_contains_resources feature.
+ When true, the provider will prevent deletion of resource groups that still contain resources.
+ Default is true (safer for production). Tests can override this variable and set it to false
+ so that test cleanup can delete resource groups that still contain nested resources.
+ EOT
+ type = bool
+ default = true
+}
+
+variable "scan_interval" {
+ description = "Time interval in milliseconds of scans for new metrics data. The default is 300000 (5 minutes) and the minimum value is 1000 milliseconds (1 second)."
+ type = number
+ default = 300000
+
+ validation {
+ condition = var.scan_interval >= 1000
+ error_message = "The scan_interval must be at least 1000 milliseconds (1 second)."
+ }
+}
\ No newline at end of file
diff --git a/azure-collection-terraform/versions.tf b/azure-collection-terraform/versions.tf
new file mode 100644
index 00000000..a03ab16e
--- /dev/null
+++ b/azure-collection-terraform/versions.tf
@@ -0,0 +1,30 @@
+terraform {
+ required_version = ">= 1.5.7"
+
+ required_providers {
+
+ sumologic = {
+ version = ">= 3.1.5"
+ source = "SumoLogic/sumologic"
+ }
+ time = {
+ source = "hashicorp/time"
+ version = ">= 0.13.1"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = ">= 3.7.2"
+ }
+
+ azurerm = {
+ source = "hashicorp/azurerm"
+ version = ">= 4.44.0"
+ }
+
+ null = {
+ source = "hashicorp/null"
+ version = ">= 3.0.0"
+ }
+
+ }
+}