diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b0c4990
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,36 @@
+version: 2
+
+# Note: Dependabot schedule only supports: daily | weekly | monthly.
+
+updates:
+ # Root python dependencies (if used)
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ # interval: "weekly" # higher frequency
+ interval: "monthly" # lowest supported frequency
+ open-pull-requests-limit: 5
+
+ # App dependencies
+ - package-ecosystem: "pip"
+ directory: "/src"
+ schedule:
+ # interval: "weekly"
+ interval: "monthly"
+ open-pull-requests-limit: 5
+
+ # A2A server dependencies
+ - package-ecosystem: "pip"
+ directory: "/src/a2a"
+ schedule:
+ # interval: "weekly"
+ interval: "monthly"
+ open-pull-requests-limit: 5
+
+ # Terraform provider updates
+ - package-ecosystem: "terraform"
+ directory: "/terraform-infrastructure"
+ schedule:
+ # interval: "weekly"
+ interval: "monthly"
+ open-pull-requests-limit: 5
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..7212186
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,40 @@
+name: CodeQL
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+# schedule:
+# - cron: "35 2 * * 1" # weekly
+
+permissions:
+ contents: read
+ security-events: write
+
+jobs:
+ analyze:
+ name: Analyze (Python)
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ["python"]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/README.md b/README.md
index bb14071..e814bd9 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Costa Rica
[](https://github.com/)
[brown9804](https://github.com/brown9804)
-Last updated: 2026-03-13
+Last updated: 2026-03-19
----------
@@ -16,6 +16,24 @@ Last updated: 2026-03-13
List of References (Click to expand)
- [Microsoft Foundry SDKs and Endpoints](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/sdk-overview?view=foundry&pivots=programming-language-python)
+- Microsoft Defender for Cloud (DevOps security):
+ - [Connect GitHub to Defender for Cloud](https://learn.microsoft.com/azure/defender-for-cloud/quickstart-onboard-github)
+ - [Connect Azure DevOps to Defender for Cloud](https://learn.microsoft.com/azure/defender-for-cloud/quickstart-onboard-devops)
+ - [DevOps security permissions and prerequisites](https://learn.microsoft.com/azure/defender-for-cloud/devops-support)
+
+
+
+
+Table of Content (Click to expand)
+
+- [Deployment Approaches (pick one)](#deployment-approaches-pick-one)
+- [Key Features](#key-features)
+- [More Security with Microsoft Defender](#more-security-with-microsoft-defender)
+ - [If the Azure portal blade errors](#if-the-azure-portal-blade-errors)
+- [About A2A Protocol](#about-a2a-protocol)
+- [Architecture](#architecture)
+- [What Happens Under the Hood](#what-happens-under-the-hood)
+- [Verification](#verification)
@@ -28,10 +46,21 @@ Last updated: 2026-03-13
> [!IMPORTANT]
> The deployment process typically takes 15-20 minutes
>
-> 1. Adjust [terraform.tfvars](./terraform-infrastructure/terraform.tfvars) values
+> 1. Pick a deployment approach (Container Apps or App Service)
+> 2. Adjust [terraform.tfvars](./terraform-infrastructure/terraform.tfvars) values
> 2. Initialize terraform with `terraform init`. Click here to [understand more about the deployment process](./terraform-infrastructure/README.md)
> 3. Run `terraform apply`, you can also leverage `terraform apply -auto-approve`.
+## Deployment Approaches (pick one)
+
+- **Container Apps (recommended default in this repo)**
+ - In `terraform-infrastructure/terraform.tfvars`: set `deployment_target = "containerapps"`
+ - Run: `cd terraform-infrastructure` then `terraform apply -var-file terraform.tfvars`
+
+- **App Service (Linux custom container)**
+ - In `terraform-infrastructure/terraform.tfvars`: set `deployment_target = "appservice"` and choose `app_service_sku` (e.g. `P0v3`)
+ - Run: `cd terraform-infrastructure` then `terraform apply -var-file terraform.tfvars`
+
## Key Features
- **Multi-agent chat orchestration (default runtime)**: WebSocket `/ws` chat app orchestrates multiple agents in a single conversation flow (routing + multi-step handoffs)
@@ -49,11 +78,52 @@ Last updated: 2026-03-13
- **UI-visible diagnostics**: Correlated `error_id` responses and optional tracebacks via `A2A_DEBUG=true` for faster troubleshooting
- **Optional A2A server included**: `src/a2a/` contains an A2A-style server framework, but it is not the default Container Apps entrypoint unless you deploy it explicitly
-## About A2A Protocol
+> [!NOTE]
+> Visibility-first rollout (recommended for demos):
+>
+> - Onboard **GitHub connector only** first to validate the Defender dashboards/workbooks.
+> - Onboard **Azure DevOps connector** only in a **sandbox org/project**.
+> - Keep **PR annotations OFF** initially (no write-back to PRs) until you decide to enable them.
+
+## More Security with Microsoft Defender
-`A2A (Agent-to-Agent) Protocol is a standardized communication framework that enables multiple AI agents to collaborate and coordinate tasks seamlessly.` Like a communication pattern for coordinating multiple agents through structured messages, delegation, and (optionally) event-driven workflows.
+> [!IMPORTANT]
+> **Defender is enabled by default in this repo's Terraform defaults.** This can incur Azure costs (Defender plans) and will provision DevOps security connector resources that still require a one-time interactive authorization step for GitHub/Azure DevOps.
+> To opt out, explicitly set the related variables to `false` in [terraform-infrastructure/terraform.tfvars](terraform-infrastructure/terraform.tfvars).
+
+This repo supports two complementary “Defender” scenarios:
+
+1. **Microsoft Defender for Cloud (workload protection / cloud posture)**
+ - This repo includes an opt-in Terraform configuration to enable Defender for Cloud plans at the subscription scope.
+ - Toggle via `enable_defender_for_cloud` in [terraform-infrastructure/terraform.tfvars](terraform-infrastructure/terraform.tfvars) (or the example `tfvars` files above).
+ - Note: enabling Defender plans can incur Azure costs.
+
+2. **Defender for Cloud DevOps Security (GHAS / ADO aggregation & reporting)**
+ - This repo can provision the **connector resources** via Terraform, but onboarding still requires **interactive authorization** to GitHub and/or Azure DevOps in the Azure portal (or providing a one-time OAuth code).
+ - This is the feature area that provides the “central dashboard” experience for GHAS-like findings (code scanning, dependency, secrets) across **organizations/projects** (not just individual repos).
+ - It can optionally add **Pull Request annotations** (a write-back action) but only when you explicitly enable/configure that feature.
+
+> [!NOTE]
+> Opt out (disable Defender): In [terraform-infrastructure/terraform.tfvars](terraform-infrastructure/terraform.tfvars), set:
+>
+> - `enable_defender_for_cloud = false`
+> - `enable_defender_devops_security = false`
+
+### If the Azure portal blade errors
+
+> If the Azure portal **Defender for Cloud → Environment settings** page fails to load with an error like: `ECS feature flags for project 'Defenders' are not initialized (ErrorAcquiringViewModel)`. Use one of these workarounds:
+
+- **Open the connector resource directly** (bypasses the Environment Settings blade):
+ - Find the connector resource IDs from Terraform outputs (look for `defender_devops_security_connector_ids`).
+ - Open in the portal using this pattern:
+ - `https://portal.azure.com/#resource//overview`
+ - Example: `.../providers/Microsoft.Security/securityConnectors/github-connector`
+- **List the connector IDs via CLI** (then open them with the URL above): `az resource list -g --resource-type Microsoft.Security/securityConnectors -o table`
+- **Browser reset**: try InPrivate/Incognito, disable extensions (ad blockers), and sign out/in.
+
+## About A2A Protocol
-This repo contains **two multi-agent implementations**:
+`A2A (Agent-to-Agent) Protocol is a standardized communication framework that enables multiple AI agents to collaborate and coordinate tasks seamlessly.` Like a communication pattern for coordinating multiple agents through structured messages, delegation, and (optionally) event-driven workflows. This repo contains **two multi-agent implementations**:
- **Default deployed chat runtime (what the Dockerfile runs)**: WebSocket `/ws` in `src/chat_app_multi_agent.py`, which routes requests and orchestrates **real Azure AI Foundry Agents** in a multi-step handoff sequence.
- **Optional A2A server implementation**: an A2A-style server under `src/a2a/` (routers, coordinator, event/task framework). Use this only if you deploy/run that entrypoint.
@@ -87,7 +157,7 @@ This repo contains **two multi-agent implementations**:
- **Product catalog helper/plugin (if used)**: `src/app/agents/product_information_plugin.py`
> [!IMPORTANT]
-> A2A vs the default deployed chat runtime
+> A2A vs the default deployed chat runtime:
>
> - **A2A server path**: event/task oriented framework under `src/a2a/` (only available if you deploy/run that server)
> - **Default path**: `/ws` WebSocket chat + routing + sequential handoffs to real Foundry agents (no event queue required for the default flow)
@@ -229,7 +299,7 @@ graph TD
-

-
Refresh Date: 2026-03-13
+

+
Refresh Date: 2026-03-19
diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md
index 011cb6c..3f3716a 100644
--- a/TROUBLESHOOTING.md
+++ b/TROUBLESHOOTING.md
@@ -353,7 +353,7 @@ terraform apply
-

-
Refresh Date: 2026-03-13
+

+
Refresh Date: 2026-03-19
diff --git a/src/a2a/status_automation.ps1 b/src/a2a/status_automation.ps1
index bb56e6f..a1b98a1 100644
--- a/src/a2a/status_automation.ps1
+++ b/src/a2a/status_automation.ps1
@@ -11,7 +11,7 @@ if () {
# Check automation endpoint
try {
- = Invoke-RestMethod -Uri "https://zava-6a7d57fb-app.azurewebsites.net/a2a/automation/status" -TimeoutSec 5
+ = Invoke-RestMethod -Uri "https://zava-9e4d78b5-app.azurewebsites.net/a2a/automation/status" -TimeoutSec 5
Write-Host "Automation Status: "
} catch {
Write-Host "Automation endpoint not accessible"
diff --git a/terraform-infrastructure/README.md b/terraform-infrastructure/README.md
index fca9bc0..2c8a5e8 100644
--- a/terraform-infrastructure/README.md
+++ b/terraform-infrastructure/README.md
@@ -55,6 +55,15 @@ Templates structure:
- terraform.tfvars `(Variable values)`: This file contains the actual values for the variables defined in `variables.tf`. By separating variable definitions and values, you can easily switch between different sets of values for different environments (e.g., development, staging, production) without changing the main configuration files.
- outputs.tf `(Output values)`: This file defines the output values that Terraform should return after applying the configuration. Outputs are useful for displaying information about the resources created, such as IP addresses, resource IDs, and other important details. They can also be used as inputs for other Terraform configurations or scripts.
+## Optional: Microsoft Defender for Cloud
+
+This Terraform setup includes an opt-in configuration to enable **Microsoft Defender for Cloud** plans at the subscription scope.
+
+> [!IMPORTANT]
+> Enabling Defender plans can incur additional costs in your Azure subscription.
+
+- To enable, set `enable_defender_for_cloud = true` in `terraform.tfvars` and optionally adjust `defender_for_cloud_plans`.
+
## How to execute it
```mermaid
@@ -127,7 +136,7 @@ graph TD;
-

-
Refresh Date: 2026-03-13
+

+
Refresh Date: 2026-03-19
diff --git a/terraform-infrastructure/main.tf b/terraform-infrastructure/main.tf
index 72da45d..f02731f 100644
--- a/terraform-infrastructure/main.tf
+++ b/terraform-infrastructure/main.tf
@@ -8,6 +8,179 @@ resource "azurerm_resource_group" "rg" {
# Subscription context for role assignments
data "azurerm_client_config" "current" {}
+locals {
+ # These subscription pricing resources are global per-subscription and often pre-exist.
+ # We keep the managed set fixed to match the import blocks below.
+ defender_for_cloud_pricing_resource_types = toset([
+ "StorageAccounts",
+ "AppServices",
+ "KeyVaults",
+ "Containers",
+ "ContainerRegistry",
+ ])
+}
+
+# Microsoft Defender for Cloud (subscription-level pricing)
+# Disabled by default because it can incur costs. Enable via terraform.tfvars.
+resource "azurerm_security_center_subscription_pricing" "defender_for_cloud" {
+ for_each = var.enable_defender_for_cloud ? local.defender_for_cloud_pricing_resource_types : toset([])
+
+ resource_type = each.value
+ tier = var.defender_for_cloud_tier
+}
+
+# Subscription pricing resources are pre-created in Azure. If they're already present,
+# Terraform must import them into state before it can manage updates.
+# These import blocks make `terraform apply` work without manual `terraform import`.
+import {
+ to = azurerm_security_center_subscription_pricing.defender_for_cloud["StorageAccounts"]
+ id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.Security/pricings/StorageAccounts"
+}
+
+import {
+ to = azurerm_security_center_subscription_pricing.defender_for_cloud["AppServices"]
+ id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.Security/pricings/AppServices"
+}
+
+import {
+ to = azurerm_security_center_subscription_pricing.defender_for_cloud["KeyVaults"]
+ id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.Security/pricings/KeyVaults"
+}
+
+import {
+ to = azurerm_security_center_subscription_pricing.defender_for_cloud["Containers"]
+ id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.Security/pricings/Containers"
+}
+
+import {
+ to = azurerm_security_center_subscription_pricing.defender_for_cloud["ContainerRegistry"]
+ id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.Security/pricings/ContainerRegistry"
+}
+
+locals {
+ enable_defender_devops_github = var.enable_defender_devops_security && var.enable_defender_devops_security_github
+ enable_defender_devops_ado = var.enable_defender_devops_security && var.enable_defender_devops_security_ado
+
+ github_devops_config_properties = merge(
+ {
+ autoDiscovery = var.defender_devops_auto_discovery
+ },
+ var.defender_devops_auto_discovery == "Disabled" ? {
+ topLevelInventoryList = tolist(var.defender_devops_github_inventory_list)
+ } : {},
+ var.defender_devops_github_oauth_code != null ? {
+ authorization = {
+ code = var.defender_devops_github_oauth_code
+ }
+ } : {}
+ )
+
+ ado_devops_config_properties = merge(
+ {
+ autoDiscovery = var.defender_devops_auto_discovery
+ },
+ var.defender_devops_auto_discovery == "Disabled" ? {
+ topLevelInventoryList = tolist(var.defender_devops_ado_inventory_list)
+ } : {},
+ var.defender_devops_ado_oauth_code != null ? {
+ authorization = {
+ code = var.defender_devops_ado_oauth_code
+ }
+ } : {}
+ )
+}
+
+# DevOps connector hierarchy identifiers
+# The Security Connector API validates hierarchyIdentifier for DevOps environments as either
+# a provider-specific resourceId or a GUID. Use stable GUIDs so `terraform apply` can succeed
+# without requiring org/repo resourceIds at provisioning time.
+resource "random_uuid" "defender_devops_github_hierarchy_id" {}
+resource "random_uuid" "defender_devops_ado_hierarchy_id" {}
+
+# Defender for Cloud DevOps security connectors (GitHub + Azure DevOps)
+# NOTE: Creating these resources is automatable, but authorization requires an interactive consent step
+# in the Azure portal (or providing a one-time OAuth code).
+
+resource "azapi_resource" "defender_devops_github_connector" {
+ count = local.enable_defender_devops_github ? 1 : 0
+ type = "Microsoft.Security/securityConnectors@2024-08-01-preview"
+ name = var.defender_devops_github_connector_name
+ location = var.location
+ parent_id = azurerm_resource_group.rg.id
+ schema_validation_enabled = false
+
+ body = jsonencode({
+ properties = {
+ environmentName = "Github"
+ hierarchyIdentifier = random_uuid.defender_devops_github_hierarchy_id.result
+ environmentData = {
+ environmentType = "GithubScope"
+ }
+ offerings = [
+ {
+ offeringType = "CspmMonitorGithub"
+ }
+ ]
+ }
+ })
+
+ depends_on = [azurerm_resource_group.rg]
+}
+
+resource "azapi_resource" "defender_devops_github_config" {
+ count = (local.enable_defender_devops_github && var.defender_devops_github_oauth_code != null) ? 1 : 0
+ type = "Microsoft.Security/securityConnectors/devops@2024-04-01"
+ name = "default"
+ parent_id = azapi_resource.defender_devops_github_connector[0].id
+ schema_validation_enabled = false
+
+ body = jsonencode({
+ properties = local.github_devops_config_properties
+ })
+
+ depends_on = [azapi_resource.defender_devops_github_connector]
+}
+
+resource "azapi_resource" "defender_devops_ado_connector" {
+ count = local.enable_defender_devops_ado ? 1 : 0
+ type = "Microsoft.Security/securityConnectors@2024-08-01-preview"
+ name = var.defender_devops_ado_connector_name
+ location = var.location
+ parent_id = azurerm_resource_group.rg.id
+ schema_validation_enabled = false
+
+ body = jsonencode({
+ properties = {
+ environmentName = "AzureDevOps"
+ hierarchyIdentifier = random_uuid.defender_devops_ado_hierarchy_id.result
+ environmentData = {
+ environmentType = "AzureDevOpsScope"
+ }
+ offerings = [
+ {
+ offeringType = "CspmMonitorAzureDevOps"
+ }
+ ]
+ }
+ })
+
+ depends_on = [azurerm_resource_group.rg]
+}
+
+resource "azapi_resource" "defender_devops_ado_config" {
+ count = (local.enable_defender_devops_ado && var.defender_devops_ado_oauth_code != null) ? 1 : 0
+ type = "Microsoft.Security/securityConnectors/devops@2024-04-01"
+ name = "default"
+ parent_id = azapi_resource.defender_devops_ado_connector[0].id
+ schema_validation_enabled = false
+
+ body = jsonencode({
+ properties = local.ado_devops_config_properties
+ })
+
+ depends_on = [azapi_resource.defender_devops_ado_connector]
+}
+
# Random suffix to mimic uniqueString(resourceGroup().id)
resource "random_id" "suffix" {
byte_length = 4
@@ -37,10 +210,10 @@ locals {
# Hash of application source & templates to trigger container rebuild when logic/UI changes
# Combine Python files and HTML templates for source tracking
- app_source_hash = sha256(join("", [
+ app_source_hash = sha256(join("", [
for f in concat(
[for py in fileset("../src", "**/*.py") : py],
- ["app/templates/index.html"] # Explicitly include the HTML template
+ ["app/templates/index.html"] # Explicitly include the HTML template
) : fileexists("../src/${f}") ? filesha256("../src/${f}") : ""
]))
product_catalog_hash = fileexists("../src/data/updated_product_catalog(in).csv") ? filesha256("../src/data/updated_product_catalog(in).csv") : "missing"
@@ -56,7 +229,7 @@ resource "azurerm_role_definition" "maas_inference_user" {
name = "${var.name_prefix}-${local.suffix}-maas-inference-user"
role_definition_id = random_uuid.maas_inference_role_id.result
scope = "/subscriptions/${data.azurerm_client_config.current.subscription_id}"
- description = "Allows calling Azure AI Foundry MaaS chat/embeddings inference endpoints."
+ description = "Allows calling Azure AI Foundry MaaS chat/embeddings inference endpoints."
permissions {
actions = []
@@ -87,7 +260,7 @@ resource "azurerm_cosmosdb_account" "cosmos" {
geo_location {
location = var.location
failover_priority = 0
- zone_redundant = false # Disable zone redundancy to avoid high demand issues for demo
+ zone_redundant = false # Disable zone redundancy to avoid high demand issues for demo
}
free_tier_enabled = false
analytical_storage_enabled = false
@@ -123,10 +296,10 @@ resource "azapi_resource" "storage" {
}
kind = "StorageV2"
properties = {
- accessTier = "Hot"
- allowSharedKeyAccess = true
- minimumTlsVersion = "TLS1_2"
- supportsHttpsTrafficOnly = true
+ accessTier = "Hot"
+ allowSharedKeyAccess = true
+ minimumTlsVersion = "TLS1_2"
+ supportsHttpsTrafficOnly = true
}
})
identity {
@@ -159,8 +332,8 @@ resource "azapi_resource" "ai_foundry" {
# Ensure allowProjectManagement is applied (some older API versions ignore it during create).
# This PATCH uses a newer api-version that supports the property and updates the existing account in place.
resource "azapi_update_resource" "ai_foundry_enable_project_mgmt" {
- type = "Microsoft.CognitiveServices/accounts@2025-06-01"
- resource_id = azapi_resource.ai_foundry.id
+ type = "Microsoft.CognitiveServices/accounts@2025-06-01"
+ resource_id = azapi_resource.ai_foundry.id
body = jsonencode({
properties = {
@@ -300,7 +473,7 @@ resource "azurerm_log_analytics_workspace" "law" {
resource_group_name = azurerm_resource_group.rg.name
sku = "PerGB2018"
retention_in_days = 90
-
+
depends_on = [
azurerm_resource_group.rg
]
@@ -312,12 +485,12 @@ resource "azurerm_application_insights" "appinsights" {
resource_group_name = azurerm_resource_group.rg.name
application_type = "web"
workspace_id = azurerm_log_analytics_workspace.law.id
-
+
# Disable billing features to avoid 404 errors
- daily_data_cap_in_gb = 1
- daily_data_cap_notifications_disabled = true
- sampling_percentage = 100
-
+ daily_data_cap_in_gb = 1
+ daily_data_cap_notifications_disabled = true
+ sampling_percentage = 100
+
lifecycle {
ignore_changes = [
tags,
@@ -328,7 +501,7 @@ resource "azurerm_application_insights" "appinsights" {
local_authentication_disabled
]
}
-
+
depends_on = [
azurerm_resource_group.rg,
azurerm_log_analytics_workspace.law
@@ -341,7 +514,7 @@ resource "azurerm_container_registry" "acr" {
location = var.location
sku = "Standard"
admin_enabled = true
-
+
depends_on = [
azurerm_resource_group.rg
]
@@ -349,11 +522,11 @@ resource "azurerm_container_registry" "acr" {
# Container Apps environment (alternative to App Service)
resource "azurerm_container_app_environment" "app_env" {
- count = local.deploy_to_container_apps ? 1 : 0
- name = "${var.name_prefix}-${local.suffix}-cae"
- location = var.location
- resource_group_name = azurerm_resource_group.rg.name
- log_analytics_workspace_id = azurerm_log_analytics_workspace.law.id
+ count = local.deploy_to_container_apps ? 1 : 0
+ name = "${var.name_prefix}-${local.suffix}-cae"
+ location = var.location
+ resource_group_name = azurerm_resource_group.rg.name
+ log_analytics_workspace_id = azurerm_log_analytics_workspace.law.id
}
resource "azurerm_user_assigned_identity" "containerapp_identity" {
@@ -365,11 +538,11 @@ resource "azurerm_user_assigned_identity" "containerapp_identity" {
# Container Apps deployment (uses ACR image)
resource "azurerm_container_app" "app" {
- count = local.deploy_to_container_apps ? 1 : 0
- name = "${var.name_prefix}-${local.suffix}-ca"
- resource_group_name = azurerm_resource_group.rg.name
+ count = local.deploy_to_container_apps ? 1 : 0
+ name = "${var.name_prefix}-${local.suffix}-ca"
+ resource_group_name = azurerm_resource_group.rg.name
container_app_environment_id = azurerm_container_app_environment.app_env[0].id
- revision_mode = "Single"
+ revision_mode = "Single"
identity {
type = "UserAssigned"
@@ -612,8 +785,8 @@ resource "azurerm_container_app" "app" {
}
registry {
- server = azurerm_container_registry.acr.login_server
- identity = azurerm_user_assigned_identity.containerapp_identity[0].id
+ server = azurerm_container_registry.acr.login_server
+ identity = azurerm_user_assigned_identity.containerapp_identity[0].id
}
secret {
@@ -691,11 +864,11 @@ resource "null_resource" "docker_image_build" {
# 4. ACR or app changes
# 5. Force rebuild on every apply (always_run ensures terraform always executes the provisioner)
triggers = {
- dockerfile_hash = local.dockerfile_hash
- app_source_hash = local.app_source_hash
- requirements_hash = fileexists("../src/requirements.txt") ? filesha256("../src/requirements.txt") : "missing"
- acr_id = azurerm_container_registry.acr.id
- always_run = timestamp() # Forces provisioner to run on every apply
+ dockerfile_hash = local.dockerfile_hash
+ app_source_hash = local.app_source_hash
+ requirements_hash = fileexists("../src/requirements.txt") ? filesha256("../src/requirements.txt") : "missing"
+ acr_id = azurerm_container_registry.acr.id
+ always_run = timestamp() # Forces provisioner to run on every apply
}
depends_on = [
@@ -703,7 +876,7 @@ resource "null_resource" "docker_image_build" {
]
provisioner "local-exec" {
- command = <<-EOT
+ command = <<-EOT
Write-Host ""
Write-Host "=========================================="
Write-Host "Building & Pushing Docker Image to ACR"
@@ -814,15 +987,15 @@ resource "azurerm_linux_web_app" "app" {
}
site_config {
- always_on = true
- http2_enabled = true
- websockets_enabled = true
+ always_on = true
+ http2_enabled = true
+ websockets_enabled = true
minimum_tls_version = "1.2"
# Ensure App Service waits for container readiness
health_check_path = "/health"
health_check_eviction_time_in_min = 10
application_stack {
- docker_image_name = "zava-chat-app:latest"
+ docker_image_name = "zava-chat-app:latest"
# Use full https URL for docker registry
docker_registry_url = "https://${local.registry_name}.azurecr.io"
}
@@ -834,45 +1007,45 @@ resource "azurerm_linux_web_app" "app" {
WEBSITES_ENABLE_APP_SERVICE_STORAGE = "false"
DOCKER_ENABLE_CI = "true"
WEBSITES_PORT = "8000"
- A2A_DEBUG = "true"
- APP_BUILD_ID = local.app_source_hash
+ A2A_DEBUG = "true"
+ APP_BUILD_ID = local.app_source_hash
# GPT Configuration (using managed identity)
- gpt_endpoint = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
- gpt_deployment = var.chat_model_deployment
- gpt_api_version = "2024-12-01-preview"
+ gpt_endpoint = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
+ gpt_deployment = var.chat_model_deployment
+ gpt_api_version = "2024-12-01-preview"
# MSFT Foundry Configuration (using managed identity)
- AZURE_AI_FOUNDRY_ENDPOINT = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
- AZURE_AI_PROJECT_NAME = local.ai_project_name
- AZURE_AI_PROJECT_ENDPOINT = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-endpoint)"
+ AZURE_AI_FOUNDRY_ENDPOINT = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
+ AZURE_AI_PROJECT_NAME = local.ai_project_name
+ AZURE_AI_PROJECT_ENDPOINT = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-endpoint)"
AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME = var.chat_model_deployment
# MSFT Foundry OpenAI Configuration (using managed identity)
- AZURE_OPENAI_CHAT_DEPLOYMENT = var.chat_model_deployment
- AZURE_OPENAI_EMBEDDING_DEPLOYMENT = "text-embedding-3-small"
- AZURE_OPENAI_ENDPOINT = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
- AZURE_OPENAI_API_VERSION = "2024-02-01"
+ AZURE_OPENAI_CHAT_DEPLOYMENT = var.chat_model_deployment
+ AZURE_OPENAI_EMBEDDING_DEPLOYMENT = "text-embedding-3-small"
+ AZURE_OPENAI_ENDPOINT = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
+ AZURE_OPENAI_API_VERSION = "2024-02-01"
# External Service Keys via Key Vault
- SEARCH_SERVICE_KEY = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/search-admin-key)"
- COSMOS_DB_KEY = var.enable_cosmos_local_auth ? "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/cosmos-primary-key)" : ""
- STORAGE_CONNECTION_STRING = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/storage-connection-string)"
+ SEARCH_SERVICE_KEY = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/search-admin-key)"
+ COSMOS_DB_KEY = var.enable_cosmos_local_auth ? "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/cosmos-primary-key)" : ""
+ STORAGE_CONNECTION_STRING = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/storage-connection-string)"
# Multi-Agent Configuration - Agent IDs from Key Vault
- USE_MULTI_AGENT = var.enable_multi_agent ? "true" : "false"
- AZURE_AI_AGENT_ENDPOINT = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-endpoint)"
- AGENT_CORA_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cora-id)"
- AGENT_INTERIOR_DESIGNER_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-interior-designer-id)"
- AGENT_INVENTORY_AGENT_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-inventory-agent-id)"
- AGENT_CUSTOMER_LOYALTY_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-customer-loyalty-id)"
- AGENT_CART_MANAGER_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cart-manager-id)"
- cora = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cora-id)"
- interior_designer = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-interior-designer-id)"
- inventory_agent = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-inventory-agent-id)"
- customer_loyalty = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-customer-loyalty-id)"
- cart_manager = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cart-manager-id)"
- CUSTOMER_ID = "CUST001"
+ USE_MULTI_AGENT = var.enable_multi_agent ? "true" : "false"
+ AZURE_AI_AGENT_ENDPOINT = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-endpoint)"
+ AGENT_CORA_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cora-id)"
+ AGENT_INTERIOR_DESIGNER_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-interior-designer-id)"
+ AGENT_INVENTORY_AGENT_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-inventory-agent-id)"
+ AGENT_CUSTOMER_LOYALTY_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-customer-loyalty-id)"
+ AGENT_CART_MANAGER_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cart-manager-id)"
+ cora = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cora-id)"
+ interior_designer = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-interior-designer-id)"
+ inventory_agent = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-inventory-agent-id)"
+ customer_loyalty = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-customer-loyalty-id)"
+ cart_manager = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault.kv.vault_uri}secrets/agent-cart-manager-id)"
+ CUSTOMER_ID = "CUST001"
}
depends_on = [
@@ -896,14 +1069,14 @@ resource "azurerm_role_assignment" "webapp_acr_pull" {
# Key Vault for central secret management
resource "azurerm_key_vault" "kv" {
- name = local.key_vault_name
- location = azurerm_resource_group.rg.location
- resource_group_name = azurerm_resource_group.rg.name
- tenant_id = data.azurerm_client_config.current.tenant_id
- sku_name = "standard"
- soft_delete_retention_days = 7
- purge_protection_enabled = true
- enable_rbac_authorization = true
+ name = local.key_vault_name
+ location = azurerm_resource_group.rg.location
+ resource_group_name = azurerm_resource_group.rg.name
+ tenant_id = data.azurerm_client_config.current.tenant_id
+ sku_name = "standard"
+ soft_delete_retention_days = 7
+ purge_protection_enabled = true
+ enable_rbac_authorization = true
public_network_access_enabled = true
network_acls {
@@ -1044,7 +1217,7 @@ resource "null_resource" "set_kv_secrets" {
# External data source for agents state
data "external" "agents_state" {
- program = ["python", "read_agents_state.py"]
+ program = ["python", "read_agents_state.py"]
depends_on = [null_resource.deploy_multi_agents]
}
@@ -1207,8 +1380,8 @@ resource "azurerm_portal_dashboard" "observability" {
dashboard_properties = jsonencode({
lenses = {
"0" = {
- order = 0
- parts = {
+ order = 0
+ parts = {
"0" = {
position = { x = 0, y = 0, width = 6, height = 4 }
metadata = {
@@ -1222,9 +1395,9 @@ resource "azurerm_portal_dashboard" "observability" {
content = {
version = "1.0.0"
chart = {
- title = "App Service Requests"
- metrics = [{ resourceMetadata = { id = azurerm_linux_web_app.app[0].id }, name = "Requests", aggregationType = "Total" }]
- timespan = { duration = "PT1H" }
+ title = "App Service Requests"
+ metrics = [{ resourceMetadata = { id = azurerm_linux_web_app.app[0].id }, name = "Requests", aggregationType = "Total" }]
+ timespan = { duration = "PT1H" }
visualization = { chartType = "Line" }
}
}
@@ -1243,8 +1416,8 @@ resource "azurerm_portal_dashboard" "observability" {
content = {
version = "1.0.0"
chart = {
- title = "CPU Percentage"
- metrics = [{ resourceMetadata = { id = azurerm_service_plan.appserviceplan[0].id }, name = "CpuPercentage", aggregationType = "Average" }]
+ title = "CPU Percentage"
+ metrics = [{ resourceMetadata = { id = azurerm_service_plan.appserviceplan[0].id }, name = "CpuPercentage", aggregationType = "Average" }]
timespan = { duration = "PT1H" }
}
}
@@ -1263,8 +1436,8 @@ resource "azurerm_portal_dashboard" "observability" {
content = {
version = "1.0.0"
chart = {
- title = "Cosmos Total Requests"
- metrics = [{ resourceMetadata = { id = azurerm_cosmosdb_account.cosmos.id }, name = "TotalRequests", aggregationType = "Total" }]
+ title = "Cosmos Total Requests"
+ metrics = [{ resourceMetadata = { id = azurerm_cosmosdb_account.cosmos.id }, name = "TotalRequests", aggregationType = "Total" }]
timespan = { duration = "PT1H" }
}
}
@@ -1283,8 +1456,8 @@ resource "azurerm_portal_dashboard" "observability" {
content = {
version = "1.0.0"
chart = {
- title = "App Insights Server Response Time"
- metrics = [{ resourceMetadata = { id = azurerm_application_insights.appinsights.id }, name = "requests/duration", aggregationType = "Average" }]
+ title = "App Insights Server Response Time"
+ metrics = [{ resourceMetadata = { id = azurerm_application_insights.appinsights.id }, name = "requests/duration", aggregationType = "Average" }]
timespan = { duration = "PT1H" }
}
}
@@ -2048,7 +2221,7 @@ resource "null_resource" "vector_index_update" {
depends_on = [null_resource.data_pipeline]
provisioner "local-exec" {
- command = <<-EOT
+ command = <<-EOT
Write-Host "Triggering vector index update if catalog changed..."
$pythonCmd = "python"
if (Get-Command python3 -ErrorAction SilentlyContinue) { $pythonCmd = "python3" }
@@ -2278,7 +2451,7 @@ resource "null_resource" "verify_real_agents" {
]
provisioner "local-exec" {
- command = <<-EOT
+ command = <<-EOT
Write-Host ""; Write-Host "=== Verifying Real Agent Provisioning (Post-Deploy) ==="; Write-Host ""
$pythonCmd = "python"
if (Get-Command python3 -ErrorAction SilentlyContinue) { $pythonCmd = "python3" }
@@ -2504,7 +2677,7 @@ resource "null_resource" "verify_multi_agent_remote" {
]
provisioner "local-exec" {
- command = <<-EOT
+ command = <<-EOT
Write-Host ""; Write-Host "=== Verifying Multi-Agent Deployment (Remote) ==="; Write-Host ""
$appUrl = "https://${local.web_app_name}.azurewebsites.net"
$agentsEndpoint = "$appUrl/agents"
@@ -2543,9 +2716,9 @@ resource "null_resource" "verify_multi_agent_remote" {
}
triggers = {
- web_app_id = azurerm_linux_web_app.app[0].id
- docker_hash = local.dockerfile_hash
- agents_code = filesha256("../src/chat_app_multi_agent.py")
+ web_app_id = azurerm_linux_web_app.app[0].id
+ docker_hash = local.dockerfile_hash
+ agents_code = filesha256("../src/chat_app_multi_agent.py")
}
}
@@ -2561,7 +2734,7 @@ resource "null_resource" "deploy_a2a_automation" {
]
provisioner "local-exec" {
- command = <<-EOT
+ command = <<-EOT
Write-Host ""
Write-Host "============================================================================"
Write-Host "=== DEPLOYING A2A AUTOMATION FRAMEWORK ==="
@@ -2856,23 +3029,23 @@ try {
}
triggers = {
- env_file_id = null_resource.create_env_file[0].id
+ env_file_id = null_resource.create_env_file[0].id
app_insights_id = azurerm_application_insights.appinsights.id
- always_run = timestamp()
+ always_run = timestamp()
}
}
# A2A Monitoring Integration with Azure
resource "azurerm_monitor_action_group" "a2a_alerts" {
count = (var.enable_a2a_automation && var.enable_monitoring_dashboards && local.deploy_to_appservice) ? 1 : 0
-
+
name = "${local.web_app_name}-a2a-alerts"
resource_group_name = azurerm_resource_group.rg.name
short_name = "a2aalerts"
webhook_receiver {
- name = "a2a-automation-webhook"
- service_uri = "https://${local.web_app_name}.azurewebsites.net/a2a/automation/webhook/alert"
+ name = "a2a-automation-webhook"
+ service_uri = "https://${local.web_app_name}.azurewebsites.net/a2a/automation/webhook/alert"
use_common_alert_schema = true
}
@@ -2882,7 +3055,7 @@ resource "azurerm_monitor_action_group" "a2a_alerts" {
# A2A System Health Alert
resource "azurerm_monitor_metric_alert" "a2a_system_health" {
count = (var.enable_a2a_automation && var.enable_monitoring_dashboards && local.deploy_to_appservice) ? 1 : 0
-
+
name = "${local.web_app_name}-a2a-health"
resource_group_name = azurerm_resource_group.rg.name
scopes = [azurerm_linux_web_app.app[0].id]
@@ -2890,26 +3063,26 @@ resource "azurerm_monitor_metric_alert" "a2a_system_health" {
severity = 2
frequency = "PT1M"
window_size = "PT5M"
-
+
criteria {
metric_namespace = "Microsoft.Web/sites"
- metric_name = "HealthCheckStatus"
+ metric_name = "HealthCheckStatus"
aggregation = "Average"
operator = "LessThan"
threshold = 1
}
-
+
action {
action_group_id = azurerm_monitor_action_group.a2a_alerts[0].id
}
-
+
depends_on = [azurerm_monitor_action_group.a2a_alerts]
}
# A2A Performance Alert
resource "azurerm_monitor_metric_alert" "a2a_performance" {
count = (var.enable_a2a_automation && var.enable_monitoring_dashboards && local.deploy_to_appservice) ? 1 : 0
-
+
name = "${local.web_app_name}-a2a-performance"
resource_group_name = azurerm_resource_group.rg.name
scopes = [azurerm_linux_web_app.app[0].id]
@@ -2917,19 +3090,19 @@ resource "azurerm_monitor_metric_alert" "a2a_performance" {
severity = 3
frequency = "PT1M"
window_size = "PT5M"
-
+
criteria {
metric_namespace = "Microsoft.Web/sites"
metric_name = "AverageResponseTime"
aggregation = "Average"
operator = "GreaterThan"
- threshold = 5000 # 5 seconds
+ threshold = 5000 # 5 seconds
}
-
+
action {
action_group_id = azurerm_monitor_action_group.a2a_alerts[0].id
}
-
+
depends_on = [azurerm_monitor_action_group.a2a_alerts]
}
@@ -2945,7 +3118,7 @@ resource "null_resource" "post_deploy_health" {
provisioner "local-exec" {
interpreter = ["PowerShell", "-Command"]
- command = <<-EOT
+ command = <<-EOT
Write-Host ""
Write-Host "============================================================================"
Write-Host "=== AUTOMATED WEB APP STARTUP FIX ==="
diff --git a/terraform-infrastructure/outputs.tf b/terraform-infrastructure/outputs.tf
index 61761a0..64e0dfe 100644
--- a/terraform-infrastructure/outputs.tf
+++ b/terraform-infrastructure/outputs.tf
@@ -48,6 +48,40 @@ output "subscription_id" {
description = "Azure subscription ID"
}
+output "defender_devops_security_next_steps" {
+ description = "Next steps to finish Defender for Cloud DevOps security onboarding (authorization requires interactive consent)."
+ value = trimspace(<<-EOT
+ Defender for Cloud DevOps security connector resources can be created by Terraform, but GitHub/Azure DevOps authorization requires a one-time interactive consent step.
+
+ 1) Azure portal: Microsoft Defender for Cloud -> Environment settings
+ https://portal.azure.com/#view/Microsoft_Azure_Security/EnvironmentSettingsBlade
+
+ 2) Find your connector(s) in resource group: ${azurerm_resource_group.rg.name}
+ - GitHub connector name: ${var.defender_devops_github_connector_name} (created when enable_defender_devops_security=true and enable_defender_devops_security_github=true)
+ - Azure DevOps connector name: ${var.defender_devops_ado_connector_name} (created when enable_defender_devops_security=true and enable_defender_devops_security_ado=true)
+
+ 3) Complete the 'Authorize' / 'Install app' step for GitHub (Org Owner) and/or the OAuth authorization for Azure DevOps (Project Collection Admin).
+
+ Important:
+ - The `Microsoft.Security/securityConnectors/devops` configuration resource requires a one-time OAuth code on create/update.
+ - This repo will only attempt to create `/devops/default` via Terraform when `defender_devops_*_oauth_code` is provided.
+ Otherwise, use the Azure portal onboarding flow to authorize.
+
+ Notes:
+ - GitHub-only is the safest starting point for demos.
+ - PR annotations are optional and can remain OFF for visibility-first mode.
+ EOT
+ )
+}
+
+output "defender_devops_security_connector_ids" {
+ description = "Resource IDs for the DevOps security connectors (if created)."
+ value = {
+ github = try(azapi_resource.defender_devops_github_connector[0].id, null)
+ ado = try(azapi_resource.defender_devops_ado_connector[0].id, null)
+ }
+}
+
output "application_insights_connection_string" {
value = azurerm_application_insights.appinsights.connection_string
description = "Application Insights connection string"
@@ -259,7 +293,7 @@ output "a2a_automation_endpoints" {
testing = "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/test/run"
deployment = "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/deploy/trigger"
performance = "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/performance"
- } : {
+ } : {
status = "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/status"
metrics = "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/metrics"
health = "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/health"
@@ -285,23 +319,23 @@ output "deployment_summary" {
description = "Summary of all deployed components"
value = {
web_application = {
- url = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}"
+ url = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}"
health_check = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}/health" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}/health"
}
ai_services = {
- foundry_endpoint = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
- project_name = local.ai_project_name
+ foundry_endpoint = "https://${local.ai_foundry_name}.cognitiveservices.azure.com/"
+ project_name = local.ai_project_name
multi_agent_enabled = var.enable_multi_agent
}
automation_framework = {
- enabled = var.enable_a2a_automation
- port = var.a2a_port
+ enabled = var.enable_a2a_automation
+ port = var.a2a_port
monitoring = var.enable_monitoring_dashboards
- testing = var.enable_continuous_testing
+ testing = var.enable_continuous_testing
endpoints = var.enable_a2a_automation ? {
- status = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/status" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/status"
+ status = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/status" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/status"
metrics = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/metrics" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/metrics"
- health = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/health" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/health"
+ health = var.deployment_target == "appservice" ? "https://${azurerm_linux_web_app.app[0].default_hostname}/a2a/automation/health" : "https://${azurerm_container_app.app[0].ingress[0].fqdn}/a2a/automation/health"
} : null
}
data_services = {
diff --git a/terraform-infrastructure/provider.tf b/terraform-infrastructure/provider.tf
index c4e645b..f3aa055 100644
--- a/terraform-infrastructure/provider.tf
+++ b/terraform-infrastructure/provider.tf
@@ -22,7 +22,7 @@ provider "azurerm" {
prevent_deletion_if_contains_resources = false
}
}
-
+
# Increase timeout for Azure API operations
skip_provider_registration = false
}
diff --git a/terraform-infrastructure/terraform.tfvars b/terraform-infrastructure/terraform.tfvars
index 381ba1a..122b245 100644
--- a/terraform-infrastructure/terraform.tfvars
+++ b/terraform-infrastructure/terraform.tfvars
@@ -2,13 +2,46 @@ resource_group_name = "RG-AI-Retail-DemoX34"
location = "eastus2"
name_prefix = "zava"
-# App Service Plan SKU (change if quota blocks this tier)
-app_service_sku = "P0v3"
+# ---------------------------
+# Deployment approach (pick one)
+# ---------------------------
+# Option A (default): Container Apps
+deployment_target = "containerapps"
-# Deployment target (appservice|containerapps)
-deployment_target = "containerapps"
+# Option B: App Service (Linux custom container)
+# deployment_target = "appservice"
+# app_service_sku = "P0v3" # change if quota blocks this tier
+
+# Note: app_service_sku is only used when deployment_target = "appservice".
# Enable multi-agent architecture
enable_multi_agent = true
+# --- Optional security hardening ---
+# Microsoft Defender for Cloud (subscription-level).
+# NOTE: In this repo, Defender is ENABLED BY DEFAULT via Terraform variable defaults and can incur costs.
+# To opt out, explicitly set:
+# enable_defender_for_cloud = false
+#
+# defender_for_cloud_tier = "Standard"
+# defender_for_cloud_plans = ["ContainerRegistry", "Containers", "AppServices", "StorageAccounts", "KeyVaults"]
+
+# Defender for Cloud DevOps security connectors (GHAS aggregation dashboard)
+# This repo can provision the connector resources, but GitHub/ADO authorization requires an interactive consent step
+# in the Azure portal unless you supply a one-time OAuth code.
+# NOTE: In this repo, DevOps security connector provisioning is ENABLED BY DEFAULT via Terraform variable defaults.
+# To opt out, explicitly set:
+# enable_defender_devops_security = false
+#
+# By default, this repo provisions BOTH GitHub and Azure DevOps connector resources.
+# You can turn off either side explicitly:
+# enable_defender_devops_security_github = true
+# enable_defender_devops_security_ado = true
+# defender_devops_github_connector_name = "github-connector"
+# defender_devops_ado_connector_name = "ado-connector"
+# defender_devops_auto_discovery = "Enabled"
+# Optional one-time OAuth codes (sensitive). Leave unset for portal authorization.
+# defender_devops_github_oauth_code = null
+# defender_devops_ado_oauth_code = null
+
# user_principal_id is optional - defaults to current Azure CLI user (az login)
diff --git a/terraform-infrastructure/variables.tf b/terraform-infrastructure/variables.tf
index 3c55d94..72356f3 100644
--- a/terraform-infrastructure/variables.tf
+++ b/terraform-infrastructure/variables.tf
@@ -99,3 +99,110 @@ variable "chat_model_deployment" {
default = "gpt-4o-mini"
}
+variable "enable_defender_for_cloud" {
+ type = bool
+ description = "Whether to enable Microsoft Defender for Cloud plans at the subscription scope (may incur costs)."
+ default = true
+}
+
+variable "defender_for_cloud_tier" {
+ type = string
+ description = "Defender for Cloud pricing tier. Use 'Standard' to enable paid plans; 'Free' to disable paid benefits while keeping the pricing resource declared."
+ default = "Standard"
+
+ validation {
+ condition = contains(["Free", "Standard"], var.defender_for_cloud_tier)
+ error_message = "defender_for_cloud_tier must be either 'Free' or 'Standard'."
+ }
+}
+
+variable "defender_for_cloud_plans" {
+ type = set(string)
+
+ description = <<-EOT
+ Defender for Cloud plans to enable via subscription pricing.
+ NOTE: Plan names are provider/API dependent. If 'terraform apply' fails on a plan name, remove it from this set.
+ EOT
+
+ # Keep the default set conservative and aligned with resources in this repo.
+ # - ContainerRegistry: ACR image scanning
+ # - Containers: container workload protection
+ # - AppServices: App Service protection
+ # - StorageAccounts: storage threat protection
+ # - KeyVaults: Key Vault threat protection
+ default = [
+ "ContainerRegistry",
+ "Containers",
+ "AppServices",
+ "StorageAccounts",
+ "KeyVaults",
+ ]
+}
+
+variable "enable_defender_devops_security" {
+ type = bool
+ description = "Whether to provision Defender for Cloud DevOps security connector scaffolding (GitHub/Azure DevOps). Authorization still requires an interactive consent step."
+ default = true
+}
+
+variable "enable_defender_devops_security_github" {
+ type = bool
+ description = "Whether to provision the GitHub DevOps security connector (requires enable_defender_devops_security=true)."
+ default = true
+}
+
+variable "enable_defender_devops_security_ado" {
+ type = bool
+ description = "Whether to provision the Azure DevOps DevOps security connector (requires enable_defender_devops_security=true)."
+ default = true
+}
+
+variable "defender_devops_auto_discovery" {
+ type = string
+ description = "Auto-discovery mode for Defender DevOps security connectors. Use 'Enabled' for full discovery, or 'Disabled' with an explicit inventory list."
+ default = "Enabled"
+
+ validation {
+ condition = contains(["Enabled", "Disabled", "NotApplicable"], var.defender_devops_auto_discovery)
+ error_message = "defender_devops_auto_discovery must be one of: Enabled, Disabled, NotApplicable."
+ }
+}
+
+variable "defender_devops_github_connector_name" {
+ type = string
+ description = "Name for the GitHub DevOps security connector resource (max 20 chars recommended for portal parity)."
+ default = "github-connector"
+}
+
+variable "defender_devops_ado_connector_name" {
+ type = string
+ description = "Name for the Azure DevOps DevOps security connector resource (max 20 chars recommended for portal parity)."
+ default = "ado-connector"
+}
+
+variable "defender_devops_github_inventory_list" {
+ type = set(string)
+ description = "Optional top-level inventory list for GitHub when auto-discovery is Disabled. Values depend on connector API version and inventoryKind."
+ default = []
+}
+
+variable "defender_devops_ado_inventory_list" {
+ type = set(string)
+ description = "Optional top-level inventory list for Azure DevOps when auto-discovery is Disabled."
+ default = []
+}
+
+variable "defender_devops_github_oauth_code" {
+ type = string
+ description = "Optional one-time OAuth authorization code for GitHub connector devops config. Only used during create/update and not returned by GET. Leave null to authorize via Azure portal UI."
+ default = null
+ sensitive = true
+}
+
+variable "defender_devops_ado_oauth_code" {
+ type = string
+ description = "Optional one-time OAuth authorization code for Azure DevOps connector devops config. Only used during create/update and not returned by GET. Leave null to authorize via Azure portal UI."
+ default = null
+ sensitive = true
+}
+