2. Azure Platform — Activity Log and Resource Log Emission
Subscription-scope activity log export, per-resource diagnostic settings, and Azure Policy enforcement so new resources cannot be born silent
Azure Platform — Activity Log and Resource Log Emission
Azure emits on two distinct planes, and both are required for AU-2 coverage of the Azure estate.
Control plane — the activity log. Every create, update, delete, and action executed through Azure Resource Manager, plus policy evaluations, Defender for Cloud alerts, and service/resource health. One diagnostic setting per subscription. This is who changed what in the estate.
Resource plane — resource logs. What happens inside a resource: Key Vault secret reads, storage-account data operations, SQL audit events, firewall flows. One diagnostic setting per resource, per resource type, with type-specific categories.
An agency that has configured only the activity log can prove a Key Vault was created and cannot prove which secrets were read out of it.
Part 1 — Activity log
Categories
| Category | Contains |
|---|---|
Administrative |
Every create / update / delete / action through Resource Manager, including all Azure RBAC changes in the subscription |
Security |
Alerts generated by Microsoft Defender for Cloud |
Policy |
All effect-action operations by Azure Policy — Audit, Deny |
ServiceHealth |
Azure service-health incidents affecting your resources |
ResourceHealth |
Resource health-status transitions — Available, Unavailable, Degraded, Unknown |
Alert |
Azure Monitor alert activations |
Autoscale |
Autoscale engine operations |
Recommendation |
Azure Advisor recommendations |
Definitions from the Azure Activity Log event schema; the enumerable list is confirmed by Get-AzEventCategory.
Administrative, Security, and Policy are the AU-2 / AU-12 core. Administrative in particular carries RBAC changes, which is the AC-2 and AC-6 evidence stream for the Azure estate — there is no separate RBAC audit log to fall back on.
The activity log is low-volume relative to resource logs and there is no meaningful cost argument for selective enablement. ServiceHealth and ResourceHealth in particular become valuable during incident reconstruction, when you need to rule out a platform cause before attributing an outage to an attack.
Portal
- In the Azure portal, open Monitor → Activity log.
- Select Export Activity Logs.
- Select the subscription, then Add diagnostic setting.
- Name the setting, check all applicable categories, choose the destination.
- Save.
Export typically begins within about 30 minutes (configuring activity log export).
CLI
The activity log needs the subscription subcommand — the plain az monitor diagnostic-settings create is resource-scoped and will not do it:
az monitor diagnostic-settings subscription create \
--name "fmp-activity-log-export" \
--location "usgovvirginia" \
--workspace "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RG}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE}" \
--logs '[
{"category": "Administrative", "enabled": true},
{"category": "Security", "enabled": true},
{"category": "Policy", "enabled": true},
{"category": "ServiceHealth", "enabled": true},
{"category": "ResourceHealth", "enabled": true},
{"category": "Alert", "enabled": true},
{"category": "Autoscale", "enabled": true},
{"category": "Recommendation", "enabled": true}
]'New-AzDiagnosticSetting cannot export the activity log
Microsoft states it plainly: “You can’t use this method for an activity log. Instead, create an Azure Resource Manager template and deploy it by using PowerShell” (diagnostic settings in Azure Monitor).
PowerShell-first shops routinely write a New-AzDiagnosticSetting call for the activity log, see it fail or silently no-op, and conclude the activity log “doesn’t support export.” Use az monitor diagnostic-settings subscription create, or a subscription-scope ARM/Bicep deployment.
Bicep at subscription scope
// activity-log-export.bicep
// az deployment sub create --location <region> --template-file activity-log-export.bicep
targetScope = 'subscription'
param workspaceId string
param settingName string = 'fmp-activity-log-export'
var categories = [
'Administrative'
'Security'
'Policy'
'ServiceHealth'
'ResourceHealth'
'Alert'
'Autoscale'
'Recommendation'
]
resource activityLogExport 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
name: settingName
properties: {
workspaceId: workspaceId
logs: [for category in categories: {
category: category
enabled: true
}]
}
}
Landing zones with a management-group hierarchy can set this once at the top level instead of per subscription: a management-group diagnostic setting “exports Azure Monitor activity log events for that management group in addition to all management groups under it in the hierarchy” (activity log).
If both a management group and a subscription beneath it carry diagnostic settings, you receive duplicate events. Microsoft’s position is that “getting duplicate events is better than missing events,” because the hierarchy property is not populated by resource providers outside Resource Manager, so their events do not propagate upward.
Deduplicate at query time rather than by removing a setting:
AzureActivity
| extend Hash = hash(dynamic_to_json(pack_all))
| summarize arg_max(TimeGenerated, *) by Hash
For a federal tenant this is the right call — an assessor accepts a deduplicated stream; nobody accepts a gap.
Part 2 — Resource logs
Resource logs are per-resource and the category list varies by resource type. Discover what a given resource supports before writing the setting:
az monitor diagnostic-settings categories list \
--resource "${RESOURCE_ID}" \
--query "value[].{name:name, type:categoryType, group:categoryGroups}" -o tableThen create the setting. allLogs is a category group meaning “every log category this resource type has,” which is the correct default for a compliance baseline — it survives Microsoft adding a category later:
az monitor diagnostic-settings create \
--name "fmp-resource-logs" \
--resource "${RESOURCE_ID}" \
--workspace "${WORKSPACE_RESOURCE_ID}" \
--logs '[{"categoryGroup": "allLogs", "enabled": true}]' \
--metrics '[{"category": "AllMetrics", "enabled": true}]' \
--export-to-resource-specific true--export-to-resource-specific true (ARM: logAnalyticsDestinationType: 'Dedicated') sends logs to purpose-built tables instead of the generic AzureDiagnostics table. Microsoft recommends this (Azure Monitor REST API guidance).
It matters for compliance work specifically: AzureDiagnostics is a shared, sparsely-populated wide table with a column limit that different resource types compete for. Once that limit is hit, new columns are silently dropped — audit fields disappear from records that otherwise look intact. Resource- specific tables have their own schema and cannot be crowded out.
The trade-off is that detection rules must name the specific table. Decide once, apply everywhere; mixing modes across a fleet is the worst outcome.
A storage account or Event Hubs namespace cannot be the destination for its own diagnostic setting — it would generate data by writing data, forever. The portal blocks this; the CLI, PowerShell, REST API, and ARM templates do not (diagnostic settings).
Automation that enumerates every resource in a subscription and applies a diagnostic setting will eventually hit the logging storage account itself. Exclude the telemetry-infrastructure resources explicitly in any at-scale loop.
Part 3 — Enforce at scale with Azure Policy
Everything above configures what exists today. A resource created tomorrow is born silent unless policy makes emission a property of the platform rather than a deployment checklist item.
Use built-in DeployIfNotExists policy initiatives, assigned at management- group scope with a managed identity holding Monitoring Contributor on the target scope and Log Analytics Contributor on the workspace:
| Policy pattern | Effect |
|---|---|
Deploy Diagnostic Settings for <resource type> to Log Analytics workspace |
DeployIfNotExists — remediates new and, via a remediation task, existing resources |
| Configure Azure Activity logs to stream to specified Log Analytics workspace | DeployIfNotExists at subscription scope |
| Audit diagnostic setting | AuditIfNotExists — reports without changing, for the assessment-only posture |
DeployIfNotExists only fires on resource writes
A DeployIfNotExists assignment evaluates when a resource is created or updated. It does not retroactively fix the estate at assignment time — existing resources stay non-compliant until you run a remediation task against the assignment.
The standard failure: policy is assigned, the compliance dashboard goes green for newly-created resources, and a year of pre-existing resources continues emitting nothing. Assign the policy and run the remediation task, then confirm the compliance percentage against the resource count.
This is also the direct implementation of FedRAMP KSI-MLA-EVC — “the configuration of machine-based information resources, especially infrastructure as code, is persistently evaluated and tested.” A policy assignment with continuous compliance evaluation is the persistent evaluation; a spreadsheet of resources somebody checked once is not.
The repo’s Azure Arc policy library covers the adjacent hybrid-server case: Azure Arc policy library.
Verify emission
// Azure control-plane emission health, by subscription and category.
AzureActivity
| where TimeGenerated > ago(24h)
| summarize Events = count, LastEvent = max(TimeGenerated)
by SubscriptionId, Category
| extend Stale = LastEvent < ago(6h)
| order by Stale desc, SubscriptionId asc, Category asc
A subscription absent from these results is not exporting. Compare the returned SubscriptionId set against the full subscription inventory — the query cannot show you a subscription that has never sent a single event, which is exactly the one you need to find:
# Subscriptions with no activity-log diagnostic setting.
for sub in $(az account list --query "[].id" -o tsv); do
count=$(az monitor diagnostic-settings subscription list \
--subscription "$sub" --query "length(value)" -o tsv 2>/dev/null || echo 0)
[ "$count" -eq 0 ] && echo "NO ACTIVITY LOG EXPORT: $sub"
doneResource-level coverage, for resources that have emitted at least once:
// Resource types currently landing in the generic diagnostics table.
AzureDiagnostics
| where TimeGenerated > ago(24h)
| summarize Events = count, LastEvent = max(TimeGenerated) by ResourceType, ResourceProvider
| order by Events desc
Neither query proves completeness — only Azure Policy compliance state does, because it evaluates against the full resource inventory rather than against what happens to have arrived. Treat the policy compliance percentage as the AU-2 coverage metric and these queries as liveness checks on top of it.
Next
Microsoft 365 unified audit — the third plane, which neither of the diagnostic-settings pipelines above touches.