The Azure Audit Gap Nobody Talks About: Why Your 90-Day Logs Won't Survive a 7-Year Audit
2025-10-26 · ~45 min read
The hidden audit gap between what Azure logs, what auditors expect, and what your governance model actually covers—plus concrete steps to close it.
This guide is part of our Azure Governance hub covering policy enforcement, compliance frameworks, and enterprise controls.
Update (October 27, 2025): Ready to fix your Activity Log retention? I've published a complete step-by-step implementation guide: SOC 2 Audit Prep: Activity Log Retention Setup (Step-by-Step). Every click, every command, every verification - the grill assembly manual version.
The Question That Starts the Panic
"Who created this storage account three months ago?"
You're sitting in a meeting. Security is investigating potential data exposure. Leadership wants answers. You confidently open Azure Portal, navigate to Activity Logs, and...
The data is gone.
Azure Activity Logs keep 90 days by default. Your regulatory requirements demand 5-7 years. And this moment - when you realize you can't answer basic audit questions - is how most organizations discover they're silently non-compliant.
The Three-Part Audit Problem
Let me break down what's actually happening in most Azure environments:
Part 1: Do You HAVE the Logs?
This is about retention configuration. Out of the box, Azure gives you:
- Activity Logs (ARM resources): 90 days in the portal
- Azure AD Audit Logs: 30 days in the portal
- That's it.
Meanwhile, regulatory frameworks demand:
- SOX: 7 years
- HIPAA: 6 years
- GDPR: Varies by data category
- PCI-DSS: 1 year online, 3 years archive
- State regulations: Often 5-7 years
The gap: You're configured for 90 days, but legally required to keep 5-7 years. Most organizations don't discover this until they're asked to produce logs they don't have.
Azure Compliance Framework Requirements: The Reality Check
Different regulations have different retention requirements. Here's what actually matters:
Compliance Framework Retention Requirements
| Framework | Minimum Retention | What Must Be Logged | Azure Default | Gap |
|---|---|---|---|---|
| SOX (Sarbanes-Oxley) | 7 years | All financial system access, changes, admin actions | 90 days Activity Logs | 6+ years missing |
| HIPAA | 6 years | PHI access, modifications, disclosures, audit trails | 90 days Activity Logs, 30 days Azure AD | 5+ years missing |
| PCI-DSS | 1 year online, 3 years archive | Cardholder data access, network activity, admin changes | 90 days Activity Logs | 11 months+ missing |
| GDPR | Varies (typically 3-7 years) | Data processing activities, consent, access requests, deletions | 90 days Activity Logs | 2+ years missing |
| FISMA | 3 years | System access, configuration changes, security events | 90 days Activity Logs | 2+ years missing |
| ISO 27001 | 1-3 years (risk-based) | Security events, access control, changes | 90 days Activity Logs | Varies |
| SOC 2 | 1 year minimum | System changes, access, security controls | 90 days Activity Logs | 9+ months missing |
| FERPA | 5 years | Student data access, modifications | 90 days Activity Logs | 4+ years missing |
What Each Framework Actually Cares About
SOX Auditors Ask For:
- Who has privileged access to financial systems?
- All changes to production financial databases in the last 7 years
- Segregation of duties evidence
- Change approval trails
Azure gaps for SOX:
- ❌ Activity Logs expire after 90 days (need 7 years)
- ❌ No built-in change approval workflow tracking
- ❌ Segregation of duties requires custom RBAC analysis
HIPAA Auditors Ask For:
- Who accessed patient data systems?
- All administrative actions in PHI environments
- Evidence of access termination when employees leave
- Encryption key access logs
Azure gaps for HIPAA:
- ❌ Activity Logs expire after 90 days (need 6 years)
- ❌ Azure AD logs expire after 30 days (need 6 years)
- ❌ Storage account data plane logs not enabled by default
- ❌ Key Vault access logs require diagnostic settings
PCI-DSS Auditors Ask For:
- Network segmentation evidence
- Cardholder data environment access logs
- Admin action trails for CDE resources
- Quarterly vulnerability scan results
Azure gaps for PCI-DSS:
- ❌ Network flow logs not enabled by default
- ❌ Storage account access logs require configuration
- ❌ No built-in quarterly reporting
- ❌ CDE tagging strategy required but not enforced
The Real Cost of Non-Compliance
Failed audit findings aren't just embarrassing - they're expensive:
| Compliance Failure | Typical Cost | Timeline to Fix |
|---|---|---|
| SOX finding | $50K-500K in remediation + potential CFO/CIO replacement | 6-12 months |
| HIPAA violation | $100-50,000 per violation (up to $1.5M annual) | 3-6 months |
| PCI-DSS non-compliance | Loss of payment processing ($100K+/month revenue) | 1-3 months |
| GDPR fine | €20M or 4% of global revenue (whichever is higher) | 6-18 months |
| SOC 2 failure | Loss of enterprise customers ($500K-5M/year) | 6-12 months |
vs. Cost to Fix Properly:
- Log retention setup: $1K-50K/year (one-time + ongoing)
- Quarterly audit drills: 8 hours/quarter
- Documentation: 16-40 hours one-time
ROI: 10-100x preventing a single compliance failure.
Part 2: Do You KNOW WHERE the Logs Are?
Even if you've configured log retention properly, there's a documentation problem. Your logs might be scattered across:
- Activity Logs in the portal (90 days)
- Log Analytics Workspace (configured retention, maybe 30-730 days)
- Storage Account archive (hopefully years)
- Azure Monitor diagnostic settings (per-resource configuration)
- Azure AD Audit Logs (separate system entirely)
When the auditor asks "show me who accessed this storage account in Q2 2023," can you immediately answer:
1. WHERE that data lives?
2. HOW to query it?
3. WHO has access to retrieve it?
Most teams spend 4 hours scrambling to find WHERE the data is before they can even start answering the actual question.
Part 3: Can You QUERY the Logs?
You've exported logs to a Log Analytics Workspace. Great! Now what?
You need to:
- Know KQL (Kusto Query Language)
- Understand the schema differences between Activity Logs and Azure AD logs
- Build custom queries for common audit scenarios
- Document those queries so you're not rebuilding them every audit cycle
The reality: Most organizations have configured retention (Part 1), but lack the query skills (Part 3) to actually extract answers when needed.
The Two Separate Audit Systems
Here's what trips up most Azure admins: You're not dealing with one audit system. You're dealing with two.
System 1: Azure Activity Logs (ARM Resources)
This covers your ARM resources:
- Virtual machines
- Storage accounts
- Virtual networks
- Key vaults
- Managed disks
- Resource groups
Where it lives: Azure Resource Manager (ARM) / Azure Monitor
Query tool: Resource Graph (for resources) + Log Analytics (for activity)
Retention default: 90 days in portal
System 2: Azure AD Audit Logs (Identity Resources)
This covers your identity layer:
- App registrations
- Service principals
- Enterprise applications
- User sign-ins
- Admin consent grants
- Role assignments
Where it lives: Azure Active Directory / Microsoft Entra ID
Query tool: Microsoft Graph API or Log Analytics (if exported)
Retention default: 30 days in portal
The problem: Most people only monitor ONE of these systems and completely miss the other.
Azure Audit Capabilities vs. Other Cloud Providers
Considering multi-cloud? Here's how Azure compares for audit and compliance.
Cloud Provider Audit Comparison
| Capability | Azure | AWS | GCP |
|---|---|---|---|
| Default Activity Log Retention | 90 days | 90 days | 400 days (Admin Activity) |
| Identity Audit Logs | Azure AD (30 days default) | CloudTrail (90 days) | Cloud Audit Logs (400 days) |
| Native Query Language | KQL (powerful) | CloudWatch Insights (limited) | Log Analytics SQL (moderate) |
| Cost for 7-Year Retention | $50-1K/month | $100-2K/month | $80-1.5K/month |
| Immutable Storage (WORM) | ✅ Yes (Blob immutability) | ✅ Yes (S3 Object Lock) | ✅ Yes (Bucket Lock) |
| Real-Time Alerting | ✅ Azure Monitor | ✅ CloudWatch Alarms | ✅ Cloud Monitoring |
| Third-Party SIEM Integration | ✅ Excellent (Sentinel, Splunk) | ✅ Excellent (multiple) | ✅ Good (Chronicle) |
| Compliance Certifications | 90+ | 143+ | 70+ |
| Built-in Compliance Dashboard | ✅ Regulatory Compliance (Defender) | ✅ Security Hub | ✅ Security Command Center |
Key Differences That Matter
Azure Strengths:
- ✅ KQL is more powerful than AWS CloudWatch Insights
- ✅ Azure AD integration is native (AWS IAM logs separate)
- ✅ Microsoft Sentinel provides built-in SIEM
- ✅ Better integration with Microsoft compliance tools (Purview)
Azure Weaknesses:
- ❌ Default retention shorter than GCP (90 days vs 400 days)
- ❌ Azure AD Premium required for advanced logging
- ❌ Fewer compliance certifications than AWS
- ❌ App registrations can't be tagged (AWS IAM roles can)
AWS Strengths:
- ✅ More compliance certifications (143 vs Azure's 90)
- ✅ CloudTrail captures everything by default
- ✅ IAM access analyzer built-in
- ✅ Longer track record with regulators
AWS Weaknesses:
- ❌ CloudWatch query language is limited
- ❌ More expensive at scale ($100-2K/month for 7 years)
- ❌ Complex pricing model (multiple log types)
GCP Strengths:
- ✅ Longest default retention (400 days for admin activity)
- ✅ Flat pricing model (easier to predict)
- ✅ Excellent BigQuery integration for log analysis
GCP Weaknesses:
- ❌ Fewer compliance certifications (70 vs Azure's 90)
- ❌ Smaller audit ecosystem (fewer third-party tools)
- ❌ Less mature identity audit capabilities
Multi-Cloud Audit Strategy
If you're running multi-cloud, you need centralized audit aggregation:
Option 1: Third-Party SIEM (Splunk, Sumo Logic, Datadog)
- Pros: Single pane of glass, cross-cloud correlation
- Cons: Expensive ($50K-500K/year), complex setup
- Best for: Large enterprises (1,000+ cloud resources)
Option 2: Native per-cloud (Azure Sentinel, AWS Security Hub, GCP Chronicle)
- Pros: Native integration, lower cost, easier setup
- Cons: Separate dashboards, manual correlation
- Best for: Mid-size (100-1,000 resources)
Option 3: Export to Common Data Lake (Azure Data Lake, S3, BigQuery)
- Pros: Flexible, supports custom analytics
- Cons: Requires custom query development
- Best for: Data-driven organizations with analytics teams
The App Registration Blind Spot
Let me tell you about the alert that drives every Azure admin crazy:
Tenable Scanner Alert: "Unused app registration detected with client secret expiring in 30 days"
You get 50 of these every month. And every single one triggers the same questions:
- Who created this app registration?
- What does it do?
- Is it still being used?
- Who owns it?
Here's why these alerts are so painful:
App Registrations Can't Be Tagged
App registrations are NOT ARM resources. They're Azure AD objects.
That means:
- ❌ No tags (can't apply Cost Center, Owner, Environment tags)
- ❌ No Resource Graph support (can't query them with KQL in Resource Graph)
- ❌ Different audit system (Azure AD logs, not Activity Logs)
- ❌ Different API (Microsoft Graph, not Azure Resource Manager)
The Ownership Problem
When you create a VM, you can tag it: Owner: john.doe@company.com
When you create an app registration, you get:
- A GUID
- A display name (if they bothered to make it descriptive)
- Maybe a description (if you're lucky)
- An "Owners" field... that's often empty
Three months later, when security asks "who created this?", you're digging through Azure AD audit logs hoping the data is still there.
The Real-World Scenario
Security Team Email: "We found an app registration 'MyApp-Test-042' with a client secret expiring in 2 days. Is this still needed?"
Your investigation process:
1. Search Azure AD Audit Logs (hoping it's within 30 days)
2. Find the creation event (maybe)
3. Look up the user who created it (probably someone who left 6 months ago)
4. Search Slack/Teams for any mention of 'MyApp-Test-042' (desperate times)
5. Ask around until someone recognizes it (maybe)
6. Make a judgment call (coin flip)
This happens every single day in large Azure environments.
The Day-to-Day Pain Points
Let me show you the questions you get constantly and why they're so painful to answer:
Question 1: "Who created this resource?"
Scenario: A storage account appears in a production resource group. Nobody recognizes it. Leadership wants to know who created it and why.
The pain:
- Check Activity Logs → Only goes back 90 days, creation was 4 months ago
- Check Resource Tags → No Owner tag applied
- Check Resource creation logs → Not exported to Log Analytics
- Answer: "I don't know. The logs are gone."
Question 2: "Who accessed this storage account?"
Scenario: Compliance asks for a list of everyone who accessed a specific storage account during Q2 2024 for audit documentation.
The pain:
- Activity Logs show ARM operations (create, delete, update) but not data plane access
- Storage Analytics logs are separate and may not be enabled
- Diagnostic settings might not be configured
- Answer: "Let me get back to you... tomorrow... maybe."
Question 3: "What did John Doe create before he left?"
Scenario: Employee departed 6 months ago. ITSM ticket: Remove all resources created by john.doe@company.com
The pain:
- Activity Logs only go back 90 days
- Resource Graph shows current owners (tags) but not who created resources
- No historical audit trail unless exported
- Answer: "I can show you resources he currently owns, but not everything he created."
Question 4: "Who granted admin consent to this app registration?"
Scenario: Security scan finds an app with excessive permissions. Need to track down who approved it and why.
The pain:
- App registrations are in Azure AD, not ARM
- Requires Azure AD Audit Logs (30-day default retention)
- If it happened 31 days ago, the data is gone
- Answer: "The audit log doesn't go back that far."
Question 5: "Is this app registration still being used?"
Scenario: 50 app registrations flagged by Tenable. Security wants to delete unused ones.
The pain:
- No built-in "last used" metric for app registrations
- Sign-in logs might show service principal activity
- No correlation between client secret and actual usage
- Answer: "We'd have to monitor sign-in logs for 30-90 days to be sure."
The pattern: You spend 4 hours scrambling to find WHERE the data is, then realize it doesn't exist or isn't accessible.
Real-World Audit Scenarios by Industry
Different industries face different audit challenges. Here's what I've seen:
Healthcare (HIPAA Compliance)
The scenario:
Regional hospital system, 15 subscriptions, 2,000 VMs, 500TB patient data in Azure storage.
Audit question: "Show me everyone who accessed patient records for John Doe in the last 6 years."
The problem:
- Activity Logs: Only ARM-level access (who created storage account)
- Data plane access: Not logged by default
- Correlation: Patient name to storage blob path requires custom mapping
- Retention: Default 90 days, need 6 years
What they implemented:
1. Storage diagnostic settings enabled on ALL storage accounts with PHI
2. Log Analytics with 730-day retention for queries
3. Immutable blob storage (WORM) for 7-year archive
4. Custom KQL query mapping patient ID → storage path → access logs
5. Quarterly testing of patient access queries
Cost: $2,500/month (500TB of storage logs adds up)
Audit result: Passed HIPAA audit with zero findings on logging
Key lesson: Data plane logging is critical for healthcare - ARM logs aren't enough.
Financial Services (SOX Compliance)
The scenario:
Investment bank, 44 subscriptions, financial reporting systems in Azure SQL, SOX Section 404 controls.
Audit question: "Demonstrate segregation of duties: Who can modify financial data AND approve those changes?"
The problem:
- Azure RBAC: Role assignments change over time
- Historical RBAC: Not tracked by default in Activity Logs
- Approvals: No built-in approval workflow in Azure
- Evidence: Need to prove controls were in place for 7 years
What they implemented:
1. Azure Policy enforcing specific RBAC roles for financial systems
2. Change approval workflow via Azure DevOps (tracks approvals)
3. Daily RBAC snapshot exported to storage (track role assignment changes)
4. Privileged Identity Management (PIM) for just-in-time admin access
5. Activity Logs + Azure AD logs exported to Log Analytics + 7-year storage
Cost: $15,000/month (enterprise-scale logging + PIM licensing)
Audit result: Passed SOX 404 audit, SOD controls documented and proven
Key lesson: RBAC changes over time - you need historical snapshots to prove segregation of duties.
Retail (PCI-DSS Compliance)
The scenario:
E-commerce company, payment processing in Azure, 200 VMs in cardholder data environment (CDE).
Audit question: "Show network segmentation between CDE and non-CDE environments for the last year."
The problem:
- Network Security Groups (NSG): Changes over time
- NSG flow logs: Not enabled by default
- Network isolation: Hard to prove retroactively
- Quarterly scans: No built-in tracking
What they implemented:
1. NSG flow logs enabled on all CDE subnets
2. Azure Policy enforcing CDE tagging (PCI-Scope: True)
3. Network Watcher traffic analytics for visualization
4. Automated NSG validation (PowerShell script, runs daily, exports results)
5. Quarterly vulnerability scan results archived in storage
Cost: $800/month (NSG flow logs are the biggest cost)
Audit result: Passed PCI-DSS audit, network segmentation proven
Key lesson: Network-level compliance requires NSG flow logs + policy enforcement.
SaaS Startup (SOC 2 Type II)
The scenario:
Fast-growing SaaS startup, 3 subscriptions, preparing for first SOC 2 audit to win enterprise customers.
Audit question: "Show evidence of security controls operating effectively for the last 12 months."
The problem:
- Company only 18 months old
- Logs only go back 90 days (default)
- No historical evidence of controls
- Limited budget ($5K/month for compliance)
What they implemented:
1. Minimum viable logging: Activity Logs + Azure AD logs to Log Analytics (90-day retention)
2. Storage archive for 1 year (meet SOC 2 minimum)
3. 5 core queries documented for common audit scenarios
4. Monthly export of key metrics (uptime, security events, changes)
5. Quarterly security reviews documented in Confluence
Cost: $300/month (kept it minimal, will expand post-funding)
Audit result: Passed SOC 2 Type II audit (first try!)
Key lesson: Start small but START. Even 90 days of proper logging beats nothing.
The Solution Architecture
Here's how to fix this before your next audit. This is not theoretical - this is what I've implemented in production handling SOX compliance.
Step 1: Configure Log Export
You need to export logs from their 30/90-day defaults to long-term storage.
Export Activity Logs to Log Analytics
# Create or identify your Log Analytics Workspace
$workspaceName = "audit-logs-workspace"
$resourceGroup = "governance-rg"
$location = "eastus"
# Create workspace if it doesn't exist
az monitor log-analytics workspace create `
--resource-group $resourceGroup `
--workspace-name $workspaceName `
--location $location `
--retention-time 730 # 2 years retention in LAW
Configure Diagnostic Settings for Activity Logs
# Export subscription-level Activity Logs to Log Analytics
$subscriptionId = (az account show --query id -o tsv)
$workspaceId = (az monitor log-analytics workspace show `
--resource-group $resourceGroup `
--workspace-name $workspaceName `
--query id -o tsv)
az monitor diagnostic-settings create `
--name "export-activity-logs" `
--resource "/subscriptions/$subscriptionId" `
--workspace $workspaceId `
--logs '[
{
"category": "Administrative",
"enabled": true
},
{
"category": "Security",
"enabled": true
},
{
"category": "ServiceHealth",
"enabled": true
},
{
"category": "Alert",
"enabled": true
},
{
"category": "Recommendation",
"enabled": true
},
{
"category": "Policy",
"enabled": true
},
{
"category": "Autoscale",
"enabled": true
},
{
"category": "ResourceHealth",
"enabled": true
}
]'
Export Azure AD Audit Logs
This requires Azure AD Premium P1 or P2 licensing.
# Configure Azure AD diagnostic settings via Portal or API
# Navigate to: Azure AD > Diagnostic settings > Add diagnostic setting
# Select:
# - AuditLogs (who did what in Azure AD)
# - SignInLogs (authentication events)
# - NonInteractiveUserSignInLogs (service principal activity)
# - ServicePrincipalSignInLogs (app registration usage)
# - ManagedIdentitySignInLogs (managed identity activity)
# Destination: Same Log Analytics Workspace
PowerShell equivalent:
# Connect to Azure AD
Connect-AzureAD
# Get the Log Analytics Workspace resource ID
$workspaceResourceId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.OperationalInsights/workspaces/$workspaceName"
# Configure diagnostic settings for Azure AD
# Note: This requires Microsoft.Graph PowerShell module
Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes "AuditLog.Read.All", "Directory.Read.All"
# Create diagnostic setting
$params = @{
Name = "export-azuread-logs"
WorkspaceId = $workspaceResourceId
Logs = @(
@{ Category = "AuditLogs"; Enabled = $true }
@{ Category = "SignInLogs"; Enabled = $true }
@{ Category = "NonInteractiveUserSignInLogs"; Enabled = $true }
@{ Category = "ServicePrincipalSignInLogs"; Enabled = $true }
@{ Category = "ManagedIdentitySignInLogs"; Enabled = $true }
)
}
# Apply at tenant level
New-MgBetaDiagnosticSetting -BodyParameter $params
Export to Storage Account for Long-Term Archive
Log Analytics is great for querying (30-730 days), but expensive for 7-year retention. Use Storage Accounts for cheap archive.
# Create storage account for audit archive
$storageAccountName = "auditarchive$(Get-Random)"
az storage account create `
--name $storageAccountName `
--resource-group $resourceGroup `
--location $location `
--sku Standard_LRS `
--kind StorageV2 `
--access-tier Cool # Cool tier for infrequent access
# Get storage account resource ID
$storageId = (az storage account show `
--name $storageAccountName `
--resource-group $resourceGroup `
--query id -o tsv)
# Update diagnostic settings to include storage
az monitor diagnostic-settings create `
--name "export-activity-logs-storage" `
--resource "/subscriptions/$subscriptionId" `
--storage-account $storageId `
--logs '[
{
"category": "Administrative",
"enabled": true,
"retentionPolicy": {
"enabled": true,
"days": 2555
}
}
]'
Cost breakdown:
- Log Analytics: ~$2.50 per GB ingested + $0.12 per GB retention per month
- Storage Account (Cool tier): ~$0.01 per GB per month
- For 7-year retention: Storage is 95% cheaper than Log Analytics
Step 2: Build Your Query Library
You need pre-built KQL queries for common audit scenarios. Don't rebuild these every time.
Query 1: Who Created This Resource?
// Find who created a specific resource by name
AzureActivity
| where OperationNameValue == "MICROSOFT.RESOURCES/DEPLOYMENTS/WRITE"
or OperationNameValue contains "CREATE"
or OperationNameValue contains "WRITE"
| where ResourceId contains "your-resource-name" // Replace with actual resource name
| where ActivityStatusValue == "Success"
| project
TimeGenerated,
Caller, // Who did it
ResourceId, // What was created
OperationNameValue, // What action
ActivityStatusValue, // Success/Failure
Claims // Full identity context
| order by TimeGenerated desc
| take 100
Query 2: All Actions by a Specific User
// Find everything a user did (useful for offboarding investigations)
AzureActivity
| where Caller contains "john.doe@company.com" // Replace with actual user
| where TimeGenerated >= ago(90d) // Adjust time range as needed
| project
TimeGenerated,
ResourceGroup,
ResourceId,
OperationNameValue,
ActivityStatusValue
| order by TimeGenerated desc
Query 3: Resource Deletions
// Track all resource deletions (critical for audit trails)
AzureActivity
| where OperationNameValue contains "DELETE"
| where ActivityStatusValue == "Success"
| where TimeGenerated >= ago(90d)
| project
TimeGenerated,
Caller,
ResourceId,
ResourceGroup,
OperationNameValue
| order by TimeGenerated desc
Query 4: Who Granted Admin Consent to an App?
// Track admin consent grants (Azure AD Audit Logs)
AuditLogs
| where OperationName == "Consent to application"
| where Result == "success"
| extend
AppName = tostring(TargetResources[0].displayName),
ConsentedBy = tostring(InitiatedBy.user.userPrincipalName),
Permissions = tostring(TargetResources[0].modifiedProperties)
| project
TimeGenerated,
ConsentedBy,
AppName,
Permissions,
CorrelationId
| order by TimeGenerated desc
Query 5: App Registration Creation and Ownership
// Track app registration creation (Azure AD Audit Logs)
AuditLogs
| where OperationName == "Add application"
| where Result == "success"
| extend
AppName = tostring(TargetResources[0].displayName),
CreatedBy = tostring(InitiatedBy.user.userPrincipalName),
AppId = tostring(TargetResources[0].id)
| project
TimeGenerated,
CreatedBy,
AppName,
AppId,
CorrelationId
| order by TimeGenerated desc
Query 6: Service Principal Sign-In Activity
// Check if an app registration is actually being used
AADServicePrincipalSignInLogs
| where AppId == "your-app-id-guid" // Replace with actual App ID
| where TimeGenerated >= ago(90d)
| summarize
SignInCount = count(),
LastSignIn = max(TimeGenerated),
UniqueResources = dcount(ResourceId)
by AppDisplayName, AppId
| project
AppDisplayName,
AppId,
SignInCount,
LastSignIn,
UniqueResources
Query 7: Storage Account Access (Data Plane)
// Track who accessed storage account data (requires diagnostic settings on storage)
StorageBlobLogs
| where AccountName == "yourstorageaccount" // Replace with actual storage account
| where TimeGenerated >= ago(30d)
| where StatusCode == 200 // Successful operations
| extend CallerIdentity = tostring(parse_json(Identity).claims.upn)
| summarize
AccessCount = count(),
LastAccess = max(TimeGenerated)
by CallerIdentity, OperationName
| order by AccessCount desc
Common Audit Questions: Quick Reference Cheat Sheet
When the auditor emails at 4 PM asking for data by tomorrow, use this:
| Auditor Question | Azure Log Source | KQL Query Type | Typical Retention Need |
|---|---|---|---|
| "Who created this resource?" | Activity Logs | Resource creation query | 90 days - 7 years |
| "Who deleted this resource?" | Activity Logs | Resource deletion query | 90 days - 7 years |
| "What did user X do?" | Activity Logs + Azure AD | User activity query | 90 days - 7 years |
| "Who accessed this storage account?" | Storage diagnostic logs | Data plane access query | 1-7 years |
| "Who has Owner role on subscription?" | Azure RBAC | Role assignment query (current state) | Real-time |
| "Who HAD Owner role 6 months ago?" | Activity Logs (role changes) | Historical role query | 6 months - 7 years |
| "Who approved this app consent?" | Azure AD Audit Logs | Admin consent query | 30 days - 7 years |
| "Is this app registration still used?" | Service Principal Sign-In Logs | Sign-in activity query | 30-90 days |
| "Show all failed login attempts" | Azure AD Sign-In Logs | Failed auth query | 30 days - 1 year |
| "Who modified this NSG rule?" | Activity Logs | NSG change query | 90 days - 3 years |
| "Show database schema changes" | Azure SQL Auditing | SQL audit query | 90 days - 7 years |
| "List all privileged access" | Azure AD PIM Logs | PIM activation query | 30 days - 1 year |
The 5 Queries Every Compliance Team Needs
Save these in your query library. You'll use them constantly:
Query 1: Last 30 Days of Resource Deletions
AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue contains "DELETE"
| where ActivityStatusValue == "Success"
| project TimeGenerated, Caller, ResourceId, ResourceGroup
| order by TimeGenerated desc
Query 2: Changes by High-Privilege Users
AzureActivity
| where TimeGenerated > ago(30d)
| where Caller in ("admin1@company.com", "admin2@company.com") // Replace with actual admins
| where ActivityStatusValue == "Success"
| summarize ChangeCount = count() by Caller, OperationNameValue
| order by ChangeCount desc
Query 3: Admin Consent Grants (Last 90 Days)
AuditLogs
| where TimeGenerated > ago(90d)
| where OperationName == "Consent to application"
| where Result == "success"
| extend AppName = tostring(TargetResources[0].displayName)
| project TimeGenerated, InitiatedBy.user.userPrincipalName, AppName
| order by TimeGenerated desc
Query 4: Failed Sign-Ins (Potential Brute Force)
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != "0" // 0 = success
| summarize FailCount = count() by UserPrincipalName, IPAddress
| where FailCount > 5
| order by FailCount desc
Query 5: Storage Account Access (Data Plane)
StorageBlobLogs
| where TimeGenerated > ago(7d)
| where StatusCode == 200
| summarize AccessCount = count() by AccountName, CallerIpAddress, OperationName
| order by AccessCount desc
Pro tip: Save these as "Saved Queries" in Log Analytics. Name them clearly: "AUDIT - Resource Deletions", "AUDIT - Admin Changes", etc.
Step 3: Document Your Logging Architecture
Create a one-page reference document that answers:
Audit Log Documentation Template:
# Azure Audit Logging Architecture
## Log Analytics Workspace
- **Name:** audit-logs-workspace
- **Resource Group:** governance-rg
- **Retention:** 730 days (2 years)
- **Access:** Security team, Compliance team, IT leadership
## Storage Account Archive
- **Name:** auditarchiveXXXXXX
- **Resource Group:** governance-rg
- **Retention:** 2,555 days (7 years)
- **Access:** Compliance team only (RBAC locked down)
## What's Collected
### Activity Logs (ARM Resources)
- All subscriptions configured with diagnostic settings
- Categories: Administrative, Security, Policy, Alerts
- Retention: 730 days in LAW, 7 years in Storage
### Azure AD Audit Logs
- AuditLogs (who did what in Azure AD)
- SignInLogs (user authentication)
- ServicePrincipalSignInLogs (app registration usage)
- Retention: 730 days in LAW, 7 years in Storage
## How to Query
### Portal Access
1. Navigate to: Log Analytics Workspace > Logs
2. Select "audit-logs-workspace"
3. Use pre-built queries from: [Link to internal wiki/SharePoint]
### Common Queries
- Who created this resource? → Query #1
- What did user X do? → Query #2
- Resource deletions → Query #3
- App consent grants → Query #4
## Quarterly Audit Process
1. Compliance team requests specific queries
2. IT runs queries from template library
3. Export results to Excel/CSV
4. Compliance validates and archives
## Contacts
- **IT Governance:** your-team@company.com
- **Compliance:** compliance@company.com
- **Azure Admin:** azure-admins@company.com
Save this document where your team can find it. SharePoint, Confluence, internal wiki - wherever you keep runbooks.
Step 4: External Tracking for App Registrations
Since app registrations can't be tagged, you need external tracking.
Option 1: Spreadsheet (Quick Start)
Create a simple tracking sheet:
| App Name | App ID | Created By | Created Date | Purpose | Owner | Status | Last Review |
|---|---|---|---|---|---|---|---|
| MyApp-Prod | guid | john.doe | 2024-03-15 | Production API | Jane Smith | Active | 2024-10-01 |
| TestApp-042 | guid | old.user | 2023-08-22 | Testing | Unknown | Review | 2024-10-01 |
Update process:
- New app registration? Add row
- Quarterly review? Update Status and Last Review
- Tenable alert? Check this sheet first
Option 2: CMDB Integration (Enterprise)
If you have a CMDB (ServiceNow, Azure DevOps, Jira), create:
- CI Type: "Azure App Registration"
- Automated sync via Microsoft Graph API
- Link to resource owner records
Option 3: Automation Script
# Export all app registrations to CSV for tracking
Connect-AzureAD
$apps = Get-AzureADApplication -All $true
$report = @()
foreach ($app in $apps) {
$owners = Get-AzureADApplicationOwner -ObjectId $app.ObjectId
$report += [PSCustomObject]@{
DisplayName = $app.DisplayName
AppId = $app.AppId
ObjectId = $app.ObjectId
CreatedDateTime = $app.CreatedDateTime
Owners = ($owners.UserPrincipalName -join "; ")
SignInAudience = $app.SignInAudience
PublisherDomain = $app.PublisherDomain
}
}
$report | Export-Csv -Path "app-registrations-inventory.csv" -NoTypeInformation
Write-Host "Exported $($report.Count) app registrations to CSV"
Run this monthly. Compare to last month's export. Flag new apps without owners.
Step 5: Test Quarterly (Audit Drills)
Don't wait for a real audit to discover your logging is broken. Run drills.
Quarterly Audit Drill Checklist:
# Q4 2025 Audit Drill
## Test Date: October 26, 2025
## Tester: [Your Name]
### Test 1: Log Retention Verification
- [ ] Verify Activity Logs retention: Query logs from 90+ days ago
- [ ] Verify Azure AD logs retention: Query AuditLogs from 30+ days ago
- [ ] Verify Storage Account archive: Check oldest logs in storage
- [ ] **Result:** Pass/Fail + Notes
### Test 2: Query Execution
- [ ] Run Query #1: Who created resource X? (pick random VM)
- [ ] Run Query #2: What did user Y do? (pick random user)
- [ ] Run Query #4: Recent admin consent grants
- [ ] **Result:** Pass/Fail + Notes
### Test 3: Documentation Access
- [ ] Locate audit documentation
- [ ] Verify links to Log Analytics Workspace
- [ ] Verify query library is up to date
- [ ] **Result:** Pass/Fail + Notes
### Test 4: App Registration Tracking
- [ ] Open app registration inventory
- [ ] Verify last update date
- [ ] Spot check 5 random apps: do owners match Azure AD?
- [ ] **Result:** Pass/Fail + Notes
### Test 5: Access Verification
- [ ] Confirm compliance team has LAW access
- [ ] Confirm storage account access is restricted
- [ ] Verify audit log export is still running
- [ ] **Result:** Pass/Fail + Notes
## Issues Found:
[List any issues discovered during drill]
## Action Items:
- [ ] Fix issue #1
- [ ] Update documentation
- [ ] Retest in 30 days
## Next Drill: January 26, 2026
Why quarterly?
- Catches configuration drift before audit season
- Validates your team knows where logs are
- Proves your retention is actually working
What This Looks Like in Production
Let me show you what this setup looks like at scale.
Enterprise Bank Example (SOX Compliance)
Environment:
- 40+ Azure subscriptions
- Multiple Active Directory domains (consolidation project)
- 300+ applications
- SOX audit requirement: 7 years
What was implemented:
- Centralized Log Analytics Workspace
- One workspace for all subscriptions
- 730-day retention (balancing cost vs query access)
-
Configured via Azure Policy (automatic for new subscriptions)
-
Storage Account Archive
- Separate storage account per compliance zone
- 7-year retention
- Immutable storage (WORM) for tamper-proof logs
-
Lifecycle management (auto-archive to Cool tier after 90 days)
-
Query Library
- 25 pre-built queries for common audit scenarios
- Stored in internal wiki with examples
-
Updated quarterly based on actual audit requests
-
App Registration Tracking
- CSV export automated monthly via Azure Automation
- Stored in SharePoint with version history
- Quarterly review with security team
-
Tenable alerts cross-referenced against this inventory
-
Azure Policy Enforcement
- Diagnostic settings automatically applied to new subscriptions
- Activity Log export mandatory via policy
- Storage account lifecycle management enforced
Cost:
- Log Analytics: ~$800/month (all subscriptions)
- Storage Account: ~$50/month (7 years of logs)
- Total: ~$850/month for complete audit compliance
ROI:
- First SOX audit: 4 hours to produce all requested logs (vs. weeks previously)
- Security incidents: Immediate answers to "who did what when"
- Offboarding: Complete audit trail of departed employee actions
Small Environment Example (Startup, 1-3 Subscriptions)
Don't overthink it if you're small. Start simple:
Minimum Viable Setup:
- One Log Analytics Workspace
- 90-day retention (free tier: 5GB/month)
- Activity Logs exported from all subscriptions
-
Azure AD logs exported (requires Azure AD Premium P1)
-
One Storage Account
- Standard LRS, Cool tier
-
1-year retention (extend as budget allows)
-
Five Core Queries
- Who created this?
- What did this user do?
- Resource deletions
- Admin consent grants
-
App registration creation
-
Simple App Tracking
- Excel spreadsheet
- Updated when Tenable alerts fire
- Quarterly manual review
Cost:
- Log Analytics: $0-50/month (depending on log volume)
- Storage Account: $5-10/month
- Total: ~$60/month or less
When to upgrade:
- Growing past 5 subscriptions → Enforce with Azure Policy
- Getting audited → Extend retention to match requirements
- Multiple teams → Add RBAC and centralized governance
The Business Case for Leadership
If you need budget approval, here's your talking points:
The Risk (What Happens If We Don't)
Scenario 1: Failed Audit
- Auditor requests logs from 18 months ago
- You can't produce them (90-day default)
- Result: Audit finding, potential compliance penalty, reputational damage
Scenario 2: Security Incident
- Breach detected, forensics team needs 6 months of logs
- Activity Logs only go back 90 days
- Result: Can't determine scope of breach, can't identify entry point, liability exposure
Scenario 3: Offboarding Investigation
- Employee leaves, manager wants to know what they had access to
- No historical audit trail
- Result: 40 hours of manual investigation, incomplete results
The Cost (What It Actually Takes)
Setup:
- Engineering time: 8-16 hours (one-time)
- Monthly cost: $50-1,000 depending on scale
- Quarterly testing: 2 hours per quarter
ROI:
- First audit: 80% time savings (hours vs. weeks)
- Security incidents: Immediate forensics capability
- Offboarding: 90% time savings (2 hours vs. 40 hours)
Budget ask:
- Small environment: $1,000/year
- Medium environment: $10,000/year
- Enterprise environment: $50,000/year
Comparison:
- One failed audit finding: $50,000-500,000 in remediation + penalties
- One security breach investigation: $100,000+ in consulting fees
- This solution: $1,000-50,000/year
Approval probability: Very high.
Audit Tool Alternatives: Azure Native vs. Third-Party
Should you use Azure's native tools or pay for third-party solutions?
Tool Comparison Matrix
| Tool Category | Azure Native | Third-Party Examples | When to Use |
|---|---|---|---|
| Log Collection | Azure Monitor, Log Analytics | Splunk, Sumo Logic, Datadog | Azure Native: <1,000 resources Third-Party: Multi-cloud or >5,000 resources |
| SIEM | Microsoft Sentinel | Splunk Enterprise Security, IBM QRadar | Azure Native: Azure-only, <10,000 resources Third-Party: Multi-cloud, complex SOC |
| Compliance Dashboards | Azure Policy + Defender | CloudHealth, Prisma Cloud, Lacework | Azure Native: Basic compliance Third-Party: Multi-framework reporting |
| Log Storage | Azure Storage (Cool/Archive) | AWS S3, Wasabi, Backblaze B2 | Azure Native: Simplicity, native integration Third-Party: Multi-cloud consistency |
| Query/Analytics | KQL in Log Analytics | Splunk SPL, Sumo Logic queries | Azure Native: Azure data only Third-Party: Cross-platform correlation |
| Alerting | Azure Monitor Alerts | PagerDuty, Opsgenie, VictorOps | Azure Native: Azure-only alerts Third-Party: Multi-tool integration |
Cost Comparison (7-Year Retention for 500GB/month logs)
| Solution | Setup Cost | Monthly Cost | Total 7-Year Cost | Pros | Cons |
|---|---|---|---|---|---|
| Azure Native (Log Analytics 2yr + Storage 7yr) | $500 | $850 | $71,700 | Simple, integrated, KQL | Azure-only, limited ML |
| Splunk Cloud | $10K | $2,500 | $220K | Powerful, mature, ML/AI | Expensive, complex, overkill for small orgs |
| Sumo Logic | $5K | $1,800 | $156K | Cloud-native, good UI | Expensive, query language learning curve |
| Datadog | $2K | $1,200 | $102K | Great UX, good integrations | Expensive for long retention |
| Elastic (ELK) | $8K (self-host setup) | $600 (compute) | $58K | Open-source, flexible | DIY maintenance, steep learning curve |
Decision Framework
Use Azure Native When:
- ✅ You're Azure-only (no multi-cloud)
- ✅ <1,000 Azure resources
- ✅ Budget <$2K/month for logging
- ✅ Team knows KQL
- ✅ Basic compliance (SOC 2, ISO 27001)
Use Third-Party When:
- ✅ Multi-cloud environment (Azure + AWS/GCP)
- ✅ >5,000 resources across clouds
- ✅ Budget >$5K/month for security
- ✅ Complex compliance (SOX, HIPAA, PCI-DSS)
- ✅ Need advanced ML/AI threat detection
- ✅ 24/7 SOC team needs specialized SIEM
Hybrid Approach:
- Azure native collection (Log Analytics + Storage)
- Export to third-party SIEM for analysis
- Best of both: native integration + advanced analytics
Real Example: When We Switched
Scenario: Started with Azure native (Log Analytics + Storage), grew to 5,000+ resources + AWS.
Why we switched to Splunk:
1. Multi-cloud correlation (Azure + AWS)
2. Advanced ML for anomaly detection
3. Security team already knew Splunk
4. Compliance team wanted single dashboard
Cost impact:
- Before: $800/month (Azure native)
- After: $3,500/month (Splunk Cloud)
- Increase: 4.4x
Was it worth it?
- ✅ Detected 3 security incidents missed by Azure native
- ✅ Reduced audit prep time from 40 hours → 8 hours
- ✅ Cross-cloud visibility prevented cloud sprawl
ROI: Positive after first prevented incident.
Common Mistakes to Avoid
I've seen these mistakes repeatedly. Don't repeat them.
Mistake #1: "We'll Set It Up When We Need It"
The problem: By the time you need audit logs, it's too late. You can't retroactively create historical data.
The fix: Set up retention BEFORE your next audit cycle. If you're audited in Q1 2026, configure this by end of Q4 2025.
Mistake #2: "We Only Need Activity Logs"
The problem: Activity Logs cover ARM resources (VMs, storage). You're missing the entire identity layer (app registrations, consent grants, role assignments).
The fix: Export BOTH Activity Logs and Azure AD Audit Logs. They're separate systems covering different types of actions.
Mistake #3: "Log Analytics Retention = Archive"
The problem: Log Analytics retention (30-730 days) is great for querying, but expensive for 5-7 year archive requirements.
The fix: Use Log Analytics for recent logs (90-730 days), then archive to Storage Account for long-term retention. It's 95% cheaper.
Mistake #4: "We'll Query the Storage Account When Needed"
The problem: Querying logs directly from Storage Account is painful. No KQL support, just raw JSON files.
The fix: Log Analytics for query access (recent logs), Storage Account for compliance archive (old logs you rarely need).
Mistake #5: "Tagging Solves Everything"
The problem: Tags are great for ARM resources, but app registrations can't be tagged. You need separate tracking.
The fix: External tracking system (spreadsheet, CMDB, automated export) for app registrations. Update it monthly.
Mistake #6: "Set It and Forget It"
The problem: Diagnostic settings get deleted. Log Analytics fills up. Storage accounts get misconfigured. No one notices until the audit.
The fix: Quarterly audit drills. Test your queries. Verify your retention. Update your documentation.
The 30-Minute Quick Start
If you're reading this during an audit crisis, here's your emergency process:
Hour 1: Verify What You Have
// Check Activity Log retention
AzureActivity
| summarize
OldestLog = min(TimeGenerated),
NewestLog = max(TimeGenerated),
DaysCovered = datetime_diff('day', max(TimeGenerated), min(TimeGenerated))
| project OldestLog, NewestLog, DaysCovered
// Check Azure AD log retention
AuditLogs
| summarize
OldestLog = min(TimeGenerated),
NewestLog = max(TimeGenerated),
DaysCovered = datetime_diff('day', max(TimeGenerated), min(TimeGenerated))
| project OldestLog, NewestLog, DaysCovered
If DaysCovered < 90: You're running on defaults. You need to configure exports NOW.
Hour 2: Configure Exports (Emergency Mode)
# Quick setup: Export logs to new Log Analytics Workspace
az monitor log-analytics workspace create \
--resource-group emergency-audit-rg \
--workspace-name emergency-audit-logs \
--location eastus \
--retention-time 730
# Get workspace ID
workspaceId=$(az monitor log-analytics workspace show \
--resource-group emergency-audit-rg \
--workspace-name emergency-audit-logs \
--query id -o tsv)
# Configure diagnostic settings for current subscription
subscriptionId=$(az account show --query id -o tsv)
az monitor diagnostic-settings create \
--name emergency-export \
--resource "/subscriptions/$subscriptionId" \
--workspace $workspaceId \
--logs '[{"category":"Administrative","enabled":true}]'
Important: This only captures NEW logs going forward. You can't recover old logs that already expired.
Hour 3: Document What You've Done
Create a one-page summary:
- What logs are being captured NOW
- Where they're going (workspace name, resource group)
- How long we'll keep them (retention setting)
- What logs we DON'T have (date ranges we lost)
Be honest in your audit: "We discovered a gap in our retention configuration. We've corrected it as of [date]. Historical logs prior to [date] are not available."
The Takeaway
Most Azure environments are silently non-compliant with audit requirements because nobody talks about the 90-day default.
The three-part problem:
1. Do you HAVE the logs? (retention configuration)
2. Do you KNOW WHERE they are? (documentation gap)
3. Can you QUERY them? (skills gap)
The solution:
1. Export Activity Logs + Azure AD logs to Log Analytics + Storage
2. Build a query library for common audit scenarios
3. Document your logging architecture
4. Track app registrations externally (they can't be tagged)
5. Test quarterly with audit drills
The cost: $50-1,000/month depending on scale.
The ROI: You'll know this was worth it the moment someone asks "who created this resource 6 months ago?" and you actually have an answer.
What's your audit logging setup look like? Are you running on the 90-day default, or have you built something more robust? Reply in the comments or shoot me an email - I'd love to hear how other organizations are handling this.
And if you found this useful, share it with your compliance team. They'll thank you during the next audit cycle.
Azure Admin Starter Kit (Free Download)
Get my KQL cheat sheet, 50 Windows + 50 Linux commands, and an Azure RACI template in one free bundle.
Get the Starter Kit →Get more Azure content like this
Join Azure pros getting practical KQL queries, cost optimization tips, and real-world solutions delivered weekly.
📊 Stop Rewriting the Same KQL Queries
Get 45+ production-ready KQL queries for Azure Resource Graph, Log Analytics, and Activity Logs. Copy-paste ready with comments and business context.
Download Complete Query LibraryPDF format • No email required • Instant download