Get-AzSubscriptionActivityLog.ps1
<#PSScriptInfo .VERSION 1.0 .GUID 376c5d6e-e03f-4c0c-a2da-9e5f8c3e8bf2 .AUTHOR TrevorJones .COMPANYNAME smsagent.blog .COPYRIGHT .TAGS AzureActivityLog .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES Az.Accounts .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES For online help, see https://docs.smsagent.blog/powershell-scripts-online-help/get-azsubscriptionactivitylog .PRIVATEDATA #> <# .DESCRIPTION Gets Activity Log events from an Azure subscription with filtering options #> [CmdletBinding(HelpUri = 'https://docs.smsagent.blog/powershell-scripts-online-help/get-azsubscriptionactivitylog')] Param ( [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,Position=0)] [ValidateScript({[guid]::TryParse($_, $([ref][guid]::Empty)) -eq $true})] [string]$TenantId, [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,Position=1)] [ValidateScript({[guid]::TryParse($_, $([ref][guid]::Empty)) -eq $true})] [string]$SubscriptionID, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=2)] [int]$TimespanHours = 6, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=3)] [switch]$IncludeProperties, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=4)] [switch]$IncludeListAndGetOperations, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=5)] [ValidateSet("Application","ManagedIdentity","Service","User",$null)] [string[]]$IdentityType, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=6)] [ValidateSet("Informational","Warning","Error","Critical")] [string[]]$Level, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=7)] [string[]]$Category, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=8)] [string[]]$Caller, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=9)] [string[]]$ResourceGroupName, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=10)] [string[]]$ResourceProviderName, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=11)] [string]$ResourceIdMatch, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=12)] [string[]]$ResourceType, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=13)] [string[]]$OperationName, [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,Position=14)] [ValidateSet("Accepted","Started","Succeeded","Failed")] [string[]]$Status ) # Suppress progress bar to speed up web requests $ProgressPreference = 'SilentlyContinue' # Check for the Az.Accounts module $AzAccountsModule = Get-Module Az.Accounts -ListAvailable -ErrorAction SilentlyContinue If ($null -eq $AzAccountsModule) { throw "Please install the Az.Accounts module" } # Function to invoke a web request with error handling Function script:Invoke-WebRequestPro { Param ($URL,$Headers,$Method) try { $WebRequest = Invoke-WebRequest -Uri $URL -Method $Method -Headers $Headers -UseBasicParsing } catch { $Response = $_ $WebRequest = [PSCustomObject]@{ Message = $response.Exception.Message StatusCode = $response.Exception.Response.StatusCode StatusDescription = $response.Exception.Response.StatusDescription } } Return $WebRequest } # Get an access token for the management API try { # requires 'reader' role or 'monitoring contributer' role or custom role. $Token = Get-AzAccessToken -ResourceUrl "https://management.azure.com" -TenantId $TenantId -ErrorAction Stop $AccessToken = $Token.Token } catch { throw $_ } # Call the management API to retrieve the events # ref: https://docs.microsoft.com/en-us/rest/api/monitor/activity-logs/list?tabs=HTTP $ApiVersion = "2017-03-01-preview" $EndDate = (Get-Date).ToUniversalTime() | Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ" $StartDate = (Get-Date).AddHours(-$TimespanHours).ToUniversalTime() | Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ" $filter = "eventTimestamp ge '$StartDate' and eventTimestamp le '$EndDate' and eventChannels eq 'Admin, Operation' and levels eq 'Critical,Error,Warning,Informational'" If ($Level) { $filter = "eventTimestamp ge '$StartDate' and eventTimestamp le '$EndDate' and eventChannels eq 'Admin, Operation' and levels eq '$level'" } If ($IncludeProperties) { $select = "caller,channels,correlationId,eventDataId,eventName,category,httpRequest,level,resourceGroupName,resourceProviderName,resourceId,resourceType,operationName,properties,status,subStatus,eventTimestamp,submissionTimestamp" } else { $select = "caller,channels,correlationId,eventDataId,eventName,category,httpRequest,level,resourceGroupName,resourceProviderName,resourceId,resourceType,operationName,status,subStatus,eventTimestamp,submissionTimestamp" } $URL = "https://management.azure.com/subscriptions/$SubscriptionId/providers/microsoft.insights/eventtypes/management/values?api-version=$ApiVersion&`$filter=$filter&`$select=$select" $headers = @{'Authorization'="Bearer " + $AccessToken} $WebRequest = Invoke-WebRequestPro -URL $URL -Headers $headers -Method GET If ($WebRequest.StatusCode -eq 200) { $Content = $WebRequest.Content | ConvertFrom-JSON If ($Content.value.length -gt 0) { # loop if there are more events to get If ($Content.nextLink) { do { $URL = $Content.nextLink $headers = @{'Authorization'="Bearer " + $AccessToken} $WebRequest = Invoke-WebRequestPro -URL $URL -Headers $headers -Method GET If ($WebRequest.StatusCode -eq 200) { $Content = $WebRequest.Content | ConvertFrom-Json [array]$Events += $Content.value } else { throw $WebRequest } } until ($null -eq $Content.nextLink) } else { [array]$Events = $Content.value } } ElseIf ($Content.value -and $Content.value.length -eq 0) { [array]$Events = $null } Else { [array]$Events = $Content } } else { throw $WebRequest } If ($Events) { # Filter out "List" and "Get Token" events unless asked not to If ($IncludeListAndGetOperations) { $FilteredEvents = $Events | Sort -Property eventTimestamp -Descending } Else { $FilteredEvents = $Events | where {$_.operationName -notmatch "List" -and $_.operationName -notmatch "Get Token"} | Sort -Property eventTimestamp -Descending } # Find caller identities with a GUID [array]$Identities = $FilteredEvents.caller | group-object -NoElement | Select -ExpandProperty name | Where {[guid]::TryParse($_, $([ref][guid]::Empty)) -eq $true} | Sort-Object # Now let's get the servicePrincipal friendly names from their GUIDs If ($Identities.Count -ge 1) { # Get an access token for Microsoft Graph try { # requires Directory.Read.All permission $Token = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com" -TenantId $TenantId -ErrorAction Stop $AccessToken = $Token.Token } catch { throw $_ } foreach ($Identity in $Identities) { # ref https://docs.microsoft.com/en-us/graph/api/serviceprincipal-list?view=graph-rest-beta&tabs=http $URL = "https://graph.microsoft.com/beta/servicePrincipals?`$filter=id eq '$Identity'&`$select=id,displayName,servicePrincipalType" $headers = @{'Authorization'="Bearer " + $AccessToken} $WebRequest = Invoke-WebRequestPro -URL $URL -Headers $headers -Method GET If ($WebRequest.StatusCode -eq 200) { $Content = $WebRequest.Content | ConvertFrom-JSON If ($Content.value.length -gt 0) { [array]$servicePrincipals += $Content.value } } else { throw $WebRequest } } } # Suppress console errors temporarily in case some events have no resourceId $ErrorActionPreferenceCurrent = $ErrorActionPreference $ErrorActionPreference = 'SilentlyContinue' # Process each event into more meaningful objects $EventArray = @() foreach ($FilteredEvent in $FilteredEvents) { # Translate the identity GUID into a friendly name If ([guid]::TryParse($FilteredEvent.caller, $([ref][guid]::Empty)) -eq $true) { $Identity = $servicePrincipals.Where({$_.id -eq $FilteredEvent.caller}) $callerString = $Identity.displayName $identityTypeString = $Identity.servicePrincipalType } ElseIf ($FilteredEvent.caller -match "@") { $callerString = $FilteredEvent.caller $identityTypeString = "User" } ElseIf ($null -eq $FilteredEvent.caller) { $callerString = $null $identityTypeString = $null } Else { $callerString = $FilteredEvent.caller $identityTypeString = "Service" } $resourceProviderNameString = $FilteredEvent.resourceProviderName.localizedValue $eventObject = [PSCustomObject]@{ caller = $callerString identityType = $identityTypeString channels = $FilteredEvent.channels eventName = $FilteredEvent.eventName.localizedValue category = $FilteredEvent.category.localizedValue level = $FilteredEvent.level resourceGroupName = $FilteredEvent.resourceGroupName resourceProviderName = $resourceProviderNameString resourceId = $FilteredEvent.resourceId.Substring($FilteredEvent.resourceId.IndexOf($resourceProviderNameString)).Replace($resourceProviderNameString,"") resourceType = $FilteredEvent.resourceType.localizedValue operationName = $FilteredEvent.operationName.localizedValue status = $FilteredEvent.status.localizedValue subStatus = $FilteredEvent.subStatus.localizedValue eventTimestamp = $FilteredEvent.eventTimestamp submissionTimestamp = $FilteredEvent.submissionTimestamp } If ($IncludeProperties) { $eventObject | Add-Member -MemberType NoteProperty -Name properties -Value $FilteredEvent.properties } $EventArray += $eventObject } $ErrorActionPreference = $ErrorActionPreferenceCurrent # Apply any requested filters to the results If ($IdentityType) { $EventArray = $EventArray | Where-Object {$_.identityType -in $IdentityType} } If ($Category) { $EventArray = $EventArray | Where-Object {$_.category -in $Category} } If ($Caller) { $EventArray = $EventArray | Where-Object {$_.caller -in $Caller} } If ($ResourceGroupName) { $EventArray = $EventArray | Where-Object {$_.resourceGroupName -in $ResourceGroupName} } If ($ResourceProviderName) { $EventArray = $EventArray | Where-Object {$_.resourceProviderName -in $ResourceProviderName} } If ($ResourceIdMatch) { $EventArray = $EventArray | Where-Object {$_.resourceId -match $ResourceIdMatch} } If ($ResourceType) { $EventArray = $EventArray | Where-Object {$_.resourceType -in $ResourceType} } If ($OperationName) { $EventArray = $EventArray | Where-Object {$_.operationName -in $OperationName} } If ($Status) { $EventArray = $EventArray | Where-Object {$_.status -in $Status} } return $EventArray } |