# Last Updated: 2023/07/18 # NOTES: # - This script looks for Recovery Services Vaults in your subscription, then sends info about the VM # backup and replication items in it, as well as info about Azure Backup Agent backups in it. # - For VM backups and Azure Backup Agent backups, it will send the latest backup date and status. # - For VM replication (site recovery), it will send the replication health and failover health status. # # - It's recommended to run this script daily, but if you want to run it at a different interval, edit # the $backupJobFailHours variable below to match the interval you're running the script on. # # - This script requires a CheckCentral "CreateActivities Only" API token (set it in $ccOauthToken). # # - This script requires an App Registration in your Azure AD, and the App Registration must have the # "Reader" role assignment on the subscription you wish to run it on. You'll need the Application ID # as well as the Application Secret. # # - You'll also need your Azure Subscription ID and Tenant ID. # # - For VM backups, your check matching condition should be: # Subject - Is Exactly - vmname: Azure VM Backup # - The check success conditions should be: # Body Text - Contains - Status: Completed # Body Text - Contains - Recent: True # # - For Azure Backup Agent backups, your check matching condition should be: # Subject - Is Exactly - vmname: C:\ Azure Backup Agent # - The check success conditions should be: # Body Text - Contains - Status: Completed # Body Text - Contains - Recent: True # # - For VM replication (site recovery), your check matching condition should be: # Subject - Is Exactly - vmname: Azure Replication # - The check success conditions should be: # Body Text - Contains - Failover Health: Normal # Body Text - Contains - Replication Health: Normal # # - You'll create one check for each VM and job type. e.g. if you have both backup and replication # enabled for a VM, then you'll have two checks for that VM. ################### # Script Settings # ################### # Recovery Point Max Time Difference $backupJobFailHours = 24 # CC API info (Read/Write token) $ccOauthToken = "" # Azure API info $subscriptionId = "" $tenantId = "" $applicationId = "" $secret = "" #################################################### # Script Processing (Don't modify below this line) # #################################################### $azureOauthToken = "" if($azureOauthToken -eq "") { $param = @{ Uri = "https://login.microsoftonline.com/$tenantId/oauth2/token?api-version=2021-10-01" Method = "Post" Body = @{ grant_type = "client_credentials" resource = "https://management.core.windows.net/" client_id = $applicationId client_secret = $secret } } $result = Invoke-RestMethod @param $azureOauthToken = $result.access_token } function Send-AzureApiRequest { [CmdletBinding()] param ( [string]$uri ) $request = @{ Uri = $uri ContentType = "application/json" Method = "GET" headers = @{ authorization = "Bearer $azureOauthToken" host = "management.azure.com" } } return Invoke-RestMethod @request } function Send-CreateActivities { [CmdletBinding()] param ( [string]$ActivitiesJson ) $request = @{ Uri = "https://api.checkcentral.cc/createActivities/?apiToken=$ccOauthToken" ContentType = "application/json" Method = "POST" Body = $ActivitiesJson } return Invoke-RestMethod @request } function Get-VaultsBySub { return Send-AzureApiRequest("https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.RecoveryServices/vaults?api-version=2016-06-01") } function Get-ReplicationProtectedItems { [CmdletBinding()] param ( [string]$ResourceGroupName, [string]$VaultName ) return Send-AzureApiRequest("https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.RecoveryServices/vaults/$VaultName/replicationProtectedItems?api-version=2021-08-01") } function Get-BackupProtectedItems { [CmdletBinding()] param ( [string]$ResourceGroupName, [string]$VaultName ) return Send-AzureApiRequest("https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.RecoveryServices/vaults/$VaultName/backupProtectedItems?api-version=2021-02-10") } function Get-SubscriptionInfo { return Send-AzureApiRequest("https://management.azure.com/subscriptions/$subscriptionId/?api-version=2020-01-01") } function Get-ResourceGroupNameFromVault { [CmdletBinding()] param ( $Vault ) if($Vault.id -match "(?<=/resourceGroups/)([^/]+)") { return $Matches[0].ToString() } return $null } $subscription = Get-SubscriptionInfo if([string]::IsNullOrEmpty($subscription)) { $subscription = "Unknown" } else { $subscription = $subscription.displayName } $vaults = Get-VaultsBySub if($vaults.value.Count -eq 0) { Write-Output("Could not find any vaults") return } $activities = @() $bodies = @() foreach($vault in $vaults.value) { $resourceGroupName = Get-ResourceGroupNameFromVault -Vault $vault if($null -eq $resourceGroupName) { Write-Output("Unable to parse resource group from vault id") continue } [datetime]$date = Get-Date $vaultBackupItems = Get-BackupProtectedItems -ResourceGroupName $resourceGroupName -VaultName $vault.name foreach($item in $vaultBackupItems.value) { if([bool]($item.properties.PSobject.Properties.name -match "lastBackupTime")) { [datetime]$lastBackupTime = [datetime]::Parse($item.properties.lastBackupTime) $backupHourDifference = ($date.ToUniversalTime() - $lastBackupTime.ToUniversalTime()).TotalHours $backupRecent = $($backupHourDifference -lt $backupJobFailHours) $recentText = "" if ($backupRecent) { $recentText = "(Less than $($backupJobFailHours)h old)" } else { $recentText = "(More than $($backupJobFailHours)h old)" } if([bool]($item.properties.PSobject.Properties.name -match "computerName")) { $computerName = $item.properties.computerName.Replace(".", "") } else { $computerName = $item.properties.friendlyName } $body = @" Name: $($item.properties.friendlyName) Subscription: $($subscription) Resource Group: $($resourceGroupName) Vault: $($vault.name) Workload Type: $($item.properties.workloadType) Computer/VM Name: $computerName Time: $(([datetime]::Parse($item.properties.lastBackupTime)).ToLocalTime()) $((Get-TimeZone).Id) Status: $($item.properties.lastBackupStatus) Recent: $backupRecent $recentText "@ if($bodies.Contains($body)) { continue } else { $bodies += ,$body } $subject = "$($item.properties.friendlyName): Azure Backup" if([bool]($item.properties.PSobject.Properties.name -match "computerName")) { $subject = "$($computerName): $($item.properties.friendlyName) Azure Backup Agent" } elseif([bool]($item.properties.workloadType -match "VM")) { $subject = "$($computerName): Azure VM Backup" } $activity = @{ subject=$subject body=$body } $activities += $activity } } $vaultReplicatedItems = Get-ReplicationProtectedItems -ResourceGroupName $resourceGroupName -VaultName $vault.name foreach($item in $vaultReplicatedItems.value) { if([bool]($item.properties.PSobject.Properties.name -match "failoverHealth") -And [bool]($item.properties.PSobject.Properties.name -match "replicationHealth")) { $body = @" Name: $($item.properties.friendlyName) Subscription: $($subscription) Resource Group: $($resourceGroupName) Vault: $($vault.name) Replication Health: $($item.properties.replicationHealth) Failover Health: $($item.properties.failoverHealth)`n`n "@ foreach($healthError in $item.properties.healthErrors) { $body += @" Error Message: $($healthError.errorMessage) Creation Time: $($healthError.creationTimeUtc) Recommended Action: $($healthError.recommendedAction)`n`n "@ } if($bodies.Contains($body)) { continue } else { $bodies += ,$body } $subject = "$($item.properties.friendlyName): Azure VM Replication" $activity = @{ subject=$subject body=$body } $activities += $activity } } } if($activities.Length -gt 0) { $payload = @{ sendNotifications=$true sendTicketingSystems=$true activities=$activities } $json = ConvertTo-Json -InputObject $payload -Compress -Depth 4 Send-CreateActivities -ActivitiesJson $json }