| applyTo | **/04-governance-constraints.md, **/04-governance-constraints.json |
| description | MANDATORY Azure Policy discovery requirements for governance constraints |
Governance Discovery Instructions
CRITICAL: Governance constraints MUST be discovered from the live Azure
environment, NOT assumed from best practices.
GATE: This is a mandatory gate. If Azure connectivity fails or policies
cannot be retrieved, STOP and inform the user.
Do NOT generate governance constraints from assumptions.
Why This Matters
Assumed governance constraints cause deployment failures. Example:
- Assumed: 4 tags required (Environment, ManagedBy, Project, Owner)
- Actual: 9 tags required via Azure Policy (environment, owner, costcenter, application,
workload, sla, backup-policy, maint-window, tech-contact)
- Result: Deployment denied by Azure Policy
Management group-inherited policies are invisible to basic queries. Example:
az policy assignment list: Returns only 5 subscription-scoped policies
- Portal shows: 19 total (includes 7 inherited from management groups)
- Missed:
MCAPSGov Deny Policies, Block Azure RM Resource Creation — actual deployment blockers!
MANDATORY Discovery Workflow
Pre-Flight: Verify Azure Connectivity
Before any policy queries, verify Azure CLI authentication:
az account show --query "{name:name, id:id, tenantId:tenantId}" -o json
If this fails, STOP. Azure connectivity is required. Do NOT proceed with assumed policies.
Step 0: Use REST API for Complete Policy Discovery (MANDATORY)
[!CAUTION]
az policy assignment list misses management group-inherited policies.
The Azure Portal "Policy | Assignments" view shows ALL effective policies including
those inherited from management groups. The CLI command az policy assignment list
only returns subscription-scoped assignments by default, even with --disable-scope-strict-match.
ALWAYS use the REST API to get the complete picture matching the portal view.
az rest --method GET \
--url "https://management.azure.com/subscriptions/\
{subscription-id}/providers/Microsoft.Authorization/\
policyAssignments?api-version=2022-06-01" \
--query "value[].{name:name, \
displayName:properties.displayName, \
scope:properties.scope, \
enforcementMode:properties.enforcementMode, \
policyDefinitionId:properties.policyDefinitionId}" \
-o json
Validation: Compare the count returned by REST API with the total shown in Azure Portal
(Policy > Assignments). If they don't match, investigate missing policies.
Step 1: Query Azure Policy Assignments
MANDATORY: Before creating 04-governance-constraints.md, query ALL effective Azure Policy
assignments in the target subscription using the REST API (Step 0 above).
Do NOT rely on `az policy assignment list` alone — it misses management group-inherited policies.
Use the REST API approach from Step 0, then for each policy with Deny or DeployIfNotExists effects,
query the actual policy definition JSON to verify impact.
For Azure Resource Graph queries (supplemental, NOT primary):
Use az graph query (Azure CLI) or Azure MCP azure-mcp/search with intent:
Query ALL Azure Policy assignments including their display names, effects (deny/audit/modify),
enforcement mode, and the actual parameter values - specifically tag names that are enforced
Step 1.1: Read Policy Definition JSON (CRITICAL - MANDATORY)
NEVER trust policy display names alone. Misleading names cause false positives.
Example: Policy named "Block Azure RM Resource Creation" actually blocks Classic resources only.
MANDATORY: For ALL policies with Deny or DeployIfNotExists effects, query the actual policy definition to verify impact.
Method 1: Azure Resource Graph (Preferred - via az graph query)
Use Azure CLI az graph query command with KQL to join policy assignments with definitions:
az graph query -q "
policyresources
| where type =~ 'microsoft.authorization/policyassignments'
| extend policyDefId = tostring(properties.policyDefinitionId)
| join kind=inner (
policyresources
| where type =~ 'microsoft.authorization/policydefinitions'
| extend policyDefId = tolower(id)
| project policyDefId,
policyRule = properties.policyRule,
description = properties.description
) on policyDefId
| where tostring(policyRule['then'].effect) =~ 'deny' or tostring(policyRule['then'].effect) =~ 'deployIfNotExists'
| project assignmentName = name,
displayName = tostring(properties.displayName),
policyDefinitionId = policyDefId,
effect = tostring(policyRule['then'].effect),
policyRule,
description
" --management-groups "<your-management-group-id>" -o json
az graph query -q "<KQL>" --subscriptions "<subscription-id>" -o json
Example ARG Query (KQL):
policyresources
| where type =~ 'microsoft.authorization/policyassignments'
| extend policyDefId = tostring(properties.policyDefinitionId)
| join kind=inner (
policyresources
| where type =~ 'microsoft.authorization/policydefinitions'
| extend policyDefId = tolower(id)
| project policyDefId,
policyRule = properties.policyRule,
description = properties.description
) on policyDefId
| where tostring(policyRule['then'].effect) =~ 'deny' or tostring(policyRule['then'].effect) =~ 'deployIfNotExists'
| project assignmentName = name,
displayName = tostring(properties.displayName),
policyDefinitionId = policyDefId,
effect = tostring(policyRule['then'].effect),
policyRule,
description
Method 2: Azure CLI (Fallback — INCOMPLETE without REST API)
[!WARNING]
az policy assignment list only returns subscription-scoped assignments.
Management group-inherited policies (often the most critical — Deny policies, tag enforcement)
are NOT returned. Always use the REST API from Step 0 as the primary discovery method.
Use CLI only for drilling into individual policy definitions after REST API discovery.
az policy assignment list \
--query "[?enforcementMode=='Default'].{\
name:name, displayName:displayName, \
definitionId:policyDefinitionId, scope:scope}" \
-o json > policy-assignments.json
for assignment in $(jq -r '.[].definitionId' policy-assignments.json); do
if [[ $assignment == *"/managementGroups/"* ]]; then
mgId=$(echo $assignment | grep -oP '/managementGroups/\K[^/]+')
policyId=$(echo $assignment | grep -oP '/policyDefinitions/\K.*')
az policy definition show \
--name "$policyId" \
--management-group "$mgId" \
--query "{displayName:displayName, description:description, policyRule:policyRule, parameters:parameters}" \
-o json
else
policyId=$(echo $assignment | grep -oP '/policyDefinitions/\K.*')
az policy definition show \
--name "$policyId" \
--query "{displayName:displayName, description:description, policyRule:policyRule, parameters:parameters}" \
-o json
fi
done
Required Analysis for Each Deny Policy
When analyzing policyRule.if conditions, extract:
-
Resource Types Affected:
"field": "type",
"equals": "Microsoft.Storage/storageAccounts"
-
Conditional Logic:
"allOf": [
{"field": "type", "equals": "Microsoft.ClassicCompute/virtualMachines"},
{"value": "[resourceGroup().tags['ringValue']]", "in": "[parameters('ringValue')]"}
]
-
Configuration Checks:
"field": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess",
"equals": "true"
Red Flags for Misleading Names:
| Policy Name Pattern | Likely Actual Behavior | Verify By Checking |
|---|
| "Block Azure RM..." | May only block Classic resources | policyRule.if contains "ClassicCompute", "ClassicStorage", etc. |
| "Require [feature]" | May only apply to specific resource types | policyRule.if.field == "type" |
| "Deny [action]" with tag reference | May only apply if specific tags exist | policyRule.if contains resourceGroup().tags |
| "Enforce [setting]" | May only modify, not deny | policyRule.then.effect == "modify" or "deployIfNotExists" |
Validation Checklist (complete before documenting policy impact):
Step 2: Extract Tag Requirements
Query specifically for tag policies:
Get all policy assignments with their display names and actual parameter values -
specifically looking for tag enforcement policies with names containing 'tag' or 'Tag'
Expected output includes:
tagName1, tagName2, etc. with actual required tag names
- Effect (deny = deployment blocked, modify = auto-remediated, audit = logged)
Step 3: Query Security Policies
Query Azure Policy assignments related to security - TLS versions, HTTPS requirements,
public access restrictions, encryption requirements, authentication methods
Step 4: Query Resource Restrictions
Query Azure Policy assignments for allowed/denied resource types, SKU restrictions,
allowed locations, and naming conventions
Required Documentation
The 04-governance-constraints.md file MUST include:
Discovery Source Section (MANDATORY)
## Discovery Source
> [!IMPORTANT]
> Governance constraints discovered via REST API including management group-inherited policies.
| Query | Result | Timestamp |
| ------------------ | ---------------------- | ---------- |
| REST API Total | {X} assignments total | {ISO-8601} |
| Subscription-scope | {X} direct assignments | {ISO-8601} |
| MG-inherited | {X} inherited policies | {ISO-8601} |
| Deny-effect | {X} blockers found | {ISO-8601} |
| Tag Policies | {X} tags required | {ISO-8601} |
| Security Policies | {X} constraints | {ISO-8601} |
**Discovery Method**: REST API (`/providers/Microsoft.Authorization/policyAssignments`)
**Subscription**: {subscription-name} (`{subscription-id}`)
**Tenant**: {tenant-id}
**Scope**: All effective (subscription + management group inherited)
**Portal Validation**: {X} assignments shown in Portal — matches REST API count: {Y/N}
GATE CHECK: If Portal Validation shows a mismatch, STOP and investigate.
All policies visible in the Portal must be captured in the governance document.
Fail-Safe: If Queries Fail
If Azure REST API or Resource Graph is unavailable:
- STOP — Do NOT proceed to implementation planning
- Document the failure in the governance constraints file
- Mark all constraints as "⚠️ UNVERIFIED - Query Failed"
- Add warning: "⛔ GATE BLOCKED: Deployment CANNOT proceed due to undiscovered policy requirements"
- Provide manual commands for the user to run:
az rest --method GET --url "https://management.azure.com/
subscriptions/{id}/providers/Microsoft.Authorization/
policyAssignments?api-version=2022-06-01" -o json
az policy assignment list --disable-scope-strict-match -o table
- Do NOT generate assumed/best-practice policies as a fallback
Validation Checklist
Before completing governance constraints, verify:
Anti-Patterns (DO NOT DO)
❌ Assumption-based constraints:
## Required Tags
Based on Azure best practices, the following tags are recommended...
✅ Discovery-based constraints:
## Required Tags
Discovered from Azure Policy assignment "JV-Inherit Multiple Tags" (effect: modify):
- environment, owner, costcenter, application, workload, sla, backup-policy, maint-window, tech-contact
Query Reference
Primary: REST API (Complete — includes MG-inherited)
SUB_ID=$(az account show --query id -o tsv)
az rest --method GET \
--url "https://management.azure.com/subscriptions/\
${SUB_ID}/providers/Microsoft.Authorization/\
policyAssignments?api-version=2022-06-01" \
--query "value[].{name:name, \
displayName:properties.displayName, \
scope:properties.scope, \
enforcementMode:properties.enforcementMode, \
policyDefinitionId:properties.policyDefinitionId}" \
-o json
az policy set-definition show \
--name "{policySetDefinitionGuid}" \
--query "{displayName:displayName, \
policyCount:policyDefinitions | length(@), \
policies:policyDefinitions[].{id:policyDefinitionReferenceId}}" \
-o json
az policy definition show --name "{policyDefinitionGuid}" \
--query "{displayName:displayName, effect:policyRule.then.effect, conditions:policyRule.if}" \
-o json
az policy definition show --name "{policyDefinitionGuid}" \
--management-group "{managementGroupId}" \
--query "{displayName:displayName, effect:policyRule.then.effect, conditions:policyRule.if}" \
-o json
Supplemental: KQL Reference Queries (ARG — subscription-scoped only)
[!WARNING]
ARG queries only return policies stored in the subscription's resource graph.
Management group-inherited policies may not appear. Use REST API above as primary.
All Policy Assignments
policyresources
| where type =~ 'microsoft.authorization/policyassignments'
| extend displayName = tostring(properties.displayName)
| extend effect = tostring(properties.parameters.effect.value)
| extend enforcementMode = tostring(properties.enforcementMode)
| project id, displayName, effect, enforcementMode, scope = properties.scope
Tag Policy Parameters
policyresources
| where type =~ 'microsoft.authorization/policyassignments'
| extend displayName = tostring(properties.displayName)
| where displayName contains 'tag' or displayName contains 'Tag'
| project displayName, parameters = properties.parameters
Security Policies
policyresources
| where type =~ 'microsoft.authorization/policyassignments'
| join kind=inner (
policyresources
| where type =~ 'microsoft.authorization/policydefinitions'
| where tostring(properties.metadata.category) in ('Security', 'Network', 'Storage')
| project definitionId = tolower(id), category = tostring(properties.metadata.category)
) on $left.policyDefinitionId == $right.definitionId
| project displayName = properties.displayName, category, effect = properties.parameters.effect.value
Policy Effect Handling (Shift-Left Enforcement)
CRITICAL: Discovered policies MUST influence the implementation plan, not just be documented.
Effect-Based Actions
| Policy Effect | Impact | Required Action |
|---|
| Deny | Deployment blocked if non-compliant | Adapt architecture OR flag exemption requirement |
| DeployIfNotExists | Missing resources auto-deployed | Include expected resources in plan |
| Modify | Resources auto-modified at deployment | Document expected modifications |
| Audit | Non-compliance logged but allowed | Document compliance expectations |
| Disabled | Policy not enforced | Note for awareness |
Critical Decision Tree
Policy with Deny Effect Discovered
↓
Extract: Policy Name, Scope, Enforcement Mode
↓
Does it apply to this deployment?
↓
├─ NO → Document for awareness, proceed
└─ YES → Does it block proposed architecture?
↓
├─ NO → Document compliance, proceed
└─ YES → Can architecture be adapted to comply?
↓
├─ YES → Update implementation plan with compliant alternative
│ Document adaptation in "## Plan Adaptations" section
│ Example: Public storage → Private endpoints
└─ NO → Flag as DEPLOYMENT BLOCKER
Add to "## Deployment Blockers" section
Status: "⚠️ CANNOT PROCEED WITHOUT EXEMPTION"
Document exemption request details
Adaptation Examples
Example 1: Storage Public Access Denied
## Plan Adaptations Based on Policies
### Architectural Changes
| Original Design | Blocking Policy | Effect | Adaptation Applied |
| ------------------- | ------------------------------ | ------ | ------------------------------------ |
| Public blob storage | "Deny public storage accounts" | Deny | Private endpoints + vNet integration |
Example 2: Required Diagnostic Settings
## Plan Adaptations Based on Policies
### Auto-Applied Resources
| Policy | Effect | Auto-Applied Resource |
| ---------------------------------------- | ----------------- | --------------------------------- |
| "Deploy diagnostic settings for Storage" | DeployIfNotExists | Log Analytics diagnostic settings |
Example 3: Deployment Blocker
## Deployment Blockers
🚫 **CRITICAL**: The following policies BLOCK this deployment:
### Policy: "Block Azure RM Resource Creation"
- **ID**: `918465337cff47588b23a6e9`
- **Effect**: Deny
- **Scope**: Management Group (root) - applies to all subscriptions
- **Enforcement Mode**: Default (enabled)
- **Impact**: Prevents ALL ARM template deployments (Bicep compiles to ARM)
- **Assessment Date**: 2026-02-05
**Resolution Options**:
1. **Request Policy Exemption** (Recommended):
- **Justification**: E2E validation of Agentic InfraOps workflow
- **Duration**: Temporary (7 days)
- **Risk Level**: Low (dev/test subscription)
- **Approval Process**: Submit via Azure Portal or contact governance team
2. **Alternative Architecture**:
- Use Azure CLI/PowerShell scripts instead of Bicep
- **Not Recommended**: Defeats purpose of IaC validation
**Status**: ⚠️ **DEPLOYMENT CANNOT PROCEED WITHOUT EXEMPTION APPROVAL**
**Next Steps**:
- [ ] User confirms exemption is in place
- [ ] OR User provides exemption approval timeline
- [ ] OR User selects alternative deployment method