Private/Read-AACKeyVaultLog.ps1
|
function Read-AACKeyVaultLog { <# .SYNOPSIS Reads Key Vault audit logs (AuditEvent) from the workspaces the vaults send them to - the workbook's Operations tab - by vault, operation and caller. .DESCRIPTION One query per workspace (Invoke-AACLogQueryBatch), over both tables a vault can write to: AzureDiagnostics (Category AuditEvent) and the resource-specific AZKVAuditLogs. column_ifexists keeps it working when a workspace lacks a column; union isfuzzy when it lacks a table. As in the workbook, an Authentication call answered 401 is left out: it is the SDK's normal challenge before it authenticates. -Workspace: @{ workspace query ID (customerId) = @(vault IDs) }. Returns @{ Rows = @(@{ VaultId; Operation; Caller; CallerIp; Count; Failed; AuthDenied; Throttled; P99Ms }); Errors = @{ customerId = reason } }. Needs Log Analytics Reader on the workspaces. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [hashtable] $Workspace, # ISO 8601 duration, e.g. P1D. [Parameter(Mandatory)] [string] $Duration ) $rows = [System.Collections.Generic.List[object]]::new() $errors = @{} foreach ($customerId in @($Workspace.Keys)) { $ids = (@($Workspace[$customerId] | ForEach-Object { "'$($_.ToLowerInvariant())'" }) -join ', ') $kql = @" union isfuzzy=true (AzureDiagnostics | where Category == 'AuditEvent' | project TimeGenerated, VaultId = tolower(ResourceId), Operation = OperationName, Status = toint(column_ifexists('httpStatusCode_d', real(null))), DurationMs = todouble(column_ifexists('DurationMs', real(null))), Caller = coalesce(tostring(column_ifexists('identity_claim_appid_g', '')), tostring(column_ifexists('identity_claim_oid_g', '')), tostring(column_ifexists('identity_claim_upn_s', ''))), CallerIp = tostring(column_ifexists('CallerIPAddress', ''))), (AZKVAuditLogs | project TimeGenerated, VaultId = tolower(_ResourceId), Operation = OperationName, Status = toint(column_ifexists('HttpStatusCode', int(null))), DurationMs = todouble(column_ifexists('DurationMs', real(null))), Caller = coalesce(tostring(column_ifexists('Identity', dynamic(null)).claim.appid), tostring(column_ifexists('Identity', dynamic(null)).claim.oid)), CallerIp = tostring(column_ifexists('CallerIpAddress', ''))) | where VaultId in ($ids) | where not (Operation == 'Authentication' and Status == 401) | summarize Count = count(), Failed = countif(Status >= 400), AuthDenied = countif(Status in (401, 403)), Throttled = countif(Status == 429), P99Ms = percentile(DurationMs, 99) by VaultId, Operation, Caller, CallerIp | top 2000 by Count "@ $answer = Invoke-AACLogQueryBatch -WorkspaceId $customerId -Query @{ audit = $kql } -Timespan $Duration if ($answer.Errors.Contains('audit')) { $errors[$customerId] = [string]$answer.Errors['audit']; continue } foreach ($r in @($answer.Rows['audit'])) { if ($null -ne $r) { $rows.Add($r) } } } @{ Rows = $rows.ToArray(); Errors = $errors } } |