There are various ways to check the compliance status of Azure policies. It is common to check directly in the portal. However, automated processes are often implemented, such as queries with PowerShell or forwarding to Azure EventGrid. The most elegant solution is to use alerts. They can trigger any action, such as sending emails/SMS or calling functions/webhooks. Alerts can be based on various rules that examine logs, check metrics or check search results from Azure Resource Graph. To check the compliance status of policies, it is recommended to search against the Azure Resource Graph. Microsoft provides sample queries for this under Azure Resource Graph sample queries for Azure Policy.

To create an alert with a query against the Azure Resource Graph, the PowerShell command New-AzScheduledQueryRule, the CLI command az monitor scheduled-query or simply the Azure Portal can be used. The problem here is that the alert must have the rights to query the resource graph. It gets these rights automatically when the alert is created via the portal. However, the rights cannot be set if it is created via PowerShell or CLI.

Therefore, I use the Scheduled Query Rules REST call for the generation of the rule, packaged in a PowerShell command. The REST call is made with the PUT method (line 73), the query and the configuration are defined in the body (lines 37-67). A system identity can be created directly here (line 39). Unfortunately, this identity is not assigned to the alert. This step must therefore be done after the creation and is implemented in line 78 and 81. The PowerShell script can be downloaded as New-AlertRuleWithIdentity.ps1 from GitHub.

[CmdletBinding()]
param (
  [parameter(Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [String]
  $SubscriptionId,

  [parameter(Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [String]
  $alertRuleName,

  [parameter(Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [String]
  $alertRuleActionGroupId,

  [parameter(Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [String]
  $alertRuleRegion,

  [parameter(Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [String]
  $alertRuleResourceGroup,

  [parameter(Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [String]
  $logSearchQuery
)

$scope = "/subscriptions/$SubscriptionId"
Set-AzContext -Subscription $SubscriptionId  | Out-Null
$query = $logSearchQuery
$body = @{
  "location"=$alertRuleRegion
  "identity"=@{"type"="systemassigned"}
  "properties"=@{
    "displayName"=$alertRuleName
    "actions"=@{
      "actionGroups"=@($alertRuleActionGroupId)
      "customProperties"=@{}
      "actionProperties"=@{}
    }
    "criteria"=@{
      "allOf"=@(@{
        "operator"="GreaterThanOrEqual"
        "query"=$query
        "threshold"=1
        "timeAggregation"="Count"
        "failingPeriods"=@{
          "minFailingPeriodsToAlert"=1
          "numberOfEvaluationPeriods"=1
        }
      })
    }
    "description"=""
    "enabled"=$true
    "autoMitigate"=$false
    "evaluationFrequency"="PT15M"
    "scopes"=@($scope)
    "severity"=1
    "windowSize"="PT15M"
  }
}

$auth=Get-AzAccessToken
$authHeader= $auth.token 
$url = "https://management.azure.com/$scope/resourceGroups/$alertRuleResourceGroup/providers/microsoft.insights/scheduledqueryrules/$alertRuleName" + "?api-version=2023-03-15-preview"
$result = Invoke-RestMethod `
  -Method Put `
  -Headers @{"Authorization"="Bearer $authHeader"} `
  -ContentType "application/json; charset=utf-8" `
  -Body (ConvertTo-Json $body -Depth 10) `
  -Uri $url
$identity = $result.identity.principalId
Write-Output "Rule Created, Assigning System Idenity $identity to Alert Rule"
Start-Sleep -Seconds (15)
New-AzRoleAssignment -Scope $scope -ObjectId $identity -RoleDefinitionName Reader | Out-Null

The call is made with the necessary parameters, whereby the query can be passed directly:

.\New-AlertRuleWithIdentity.ps1 -SubscriptionId ea393b52-... -alertRuleName "ar-tzuehlke" -alertRuleActionGroupId /subscriptions/ea393b52-.../resourceGroups/<RESOURCE GROUP>/providers/microsoft.insights/actiongroups/ag-tzuehlke -alertRuleRegion "westeurope" -alertRuleResourceGroup "rg-tzuehlke" -logSearchQuery @"
>> arg("").PolicyResources
>> | where type =~ 'Microsoft.PolicyInsights/PolicyStates'
>> | extend complianceState = tostring(properties.complianceState)
>> | extend
>>   resourceId = tostring(properties.resourceId),
>>   policyAssignmentId = tostring(properties.policyAssignmentId),
>>   policyAssignmentScope = tostring(properties.policyAssignmentScope),
>>   policyAssignmentName = tostring(properties.policyAssignmentName),
>>   policyDefinitionId = tostring(properties.policyDefinitionId),
>>   policyDefinitionReferenceId = tostring(properties.policyDefinitionReferenceId),
>>   stateWeight = iff(complianceState == 'NonCompliant', int(300), iff(complianceState == 'Compliant', int(200), iff(complianceState == 'Conflict', int(100), iff(complianceState == 'Exempt', int(50), int(0)))))
>> | summarize max(stateWeight) by resourceId, policyAssignmentId, policyAssignmentScope, policyAssignmentName
>> | summarize counts = count() by policyAssignmentId, policyAssignmentScope, max_stateWeight, policyAssignmentName
>> | summarize overallStateWeight = max(max_stateWeight),
>> nonCompliantCount = sumif(counts, max_stateWeight == 300),
>> compliantCount = sumif(counts, max_stateWeight == 200),
>> conflictCount = sumif(counts, max_stateWeight == 100),
>> exemptCount = sumif(counts, max_stateWeight == 50) by policyAssignmentId, policyAssignmentScope, policyAssignmentName
>> | extend totalResources = todouble(nonCompliantCount + compliantCount + conflictCount + exemptCount)
>> | extend compliancePercentage = iff(totalResources == 0, todouble(100), 100 * todouble(compliantCount + exemptCount) / totalResources)
>> | project policyAssignmentName, scope = policyAssignmentScope,
>> complianceState = iff(overallStateWeight == 300, 'noncompliant', iff(overallStateWeight == 200, 'compliant', iff(overallStateWeight == 100, 'conflict', iff(overallStateWeight == 50, 'exempt', 'notstarted')))),
>> compliancePercentage,
>> compliantCount,
>> nonCompliantCount,
>> conflictCount,
>> exemptCount
>> "@
Rule Created, Assigning System Idenity 4d76174b-8c9f-4b7e-b549-ac34229679e8 to Alert Rule