
Update: as the AzureAD PowerShell is being deprecated, I made an updated version which can be found here
Backing up AAD Conditional Access policies is relatively straightforward with Get-AzureADMSConditionalAccessPolicy cmdlet (don’t forget to update your AzureAD module if the cmdlet is not recognized). In this post, I want to share my own backup “solution” which can detect changes based on the previously exported settings.
TL;DR
– Ensure you have appropriate permissions to read AAD CAs
– Make sure to use up-to-date AzureAD PowerShell module
– Modify $backupDir variable accordingly
The script is here
Explained
First, we define the directory where policies will be exported, which will be created if it does not exist:$backupDir = "C:\AAD_CA"
if (!(Test-Path $backupDir)){mkdir $backupDir}
Next, we connect to AzureAD. Enter appropriate credentials in popup windowConnect-AzureAD
The next part is a function declaration, which imports the previous exports from the backup directory as a custom object:
function Import-AADCABackups {
gci -File -Recurse $backupDir -Include *.json | % {
[pscustomobject]@{
ID = ($_.Name.Split(""))[0] Version =[datetime]::ParseExact( ($.BaseName.Split(""))[1], 'yyyyMMddHHmm', $null)
JSON = Get-Content $.FullName
Name = (Get-item $_.Directory).Name
}
}
}
The main function is called Backup-AADCAs which has an optional parameter -ChangedOnly
function Backup-AADCAs {
Param(
[Parameter(Mandatory=$false)]
[switch]$ChangedOnly
)
It first imports the previous backups using the Import-AADCABackups function, then the actual ones using Get-AzureADMSConditionalAccessPolicy
$import_CABackups = Import-AADCABackups
$AAD_CAs = Get-AzureADMSConditionalAccessPolicy
After storing the actual date in $strDate variable, we loop through each actual policy:
– create a subdirectory with the same name as the policy
– convert the policy to JSON, store it in $CA_JSON variable
foreach ($CA in $AAD_CAs){
#create backup directory if it does not exist
if (!(Test-Path "$backupDir\$($CA.displayname)")){New-item -ItemType Directory -Path
"$backupDir\$($CA.displayname)" >> $null }
#load JSON
$CA_JSON = $CA | ConvertTo-Json -Depth 6 -Compress
If the function is called with -ChangedOnly parameter, the following happens:
– try to find an existing backup of the policy based on its ID and select the latest one:
$import_CABackup_latest_JSON = ($import_CABackups.where({$_.ID -eq $CA.id}) | sort version | select -Last 1).JSON
– if it didn’t find a match, the CA in the loop is considered new and backup is created:
#New CA
if ($import_CABackup_latest_JSON -eq $null){
Write-Host "New policy found: $($CA.DisplayName)" -ForegroundColor Green
Out-File -InputObject $CA_JSON -Encoding utf8 -FilePath "$backupDir\$($CA.displayname)\$($ca.id)_$strdate.json"
}
– if there was a match, but the latest one’s content differs from the actual one, then it is considered to be changed:
#Difference found
if (([bool]$import_CABackup_latest_JSON) -and ($import_CABackup_latest_JSON -ne $CA_JSON)){
Write-Host "Found difference for $($CA.DisplayName)" -ForegroundColor Yellow
Out-File -InputObject $CA_JSON -Encoding utf8 -FilePath "$backupDir\$($CA.displayname)\$($ca.id)_$strdate.json"
}
– if there was a match and it’s content is the same as the actual one, then there was no change:
#No difference found
if (([bool]$import_CABackup_latest_JSON) -and ($import_CABackup_latest_JSON -eq $CA_JSON)){
Write-Host "No difference found for $($CA.DisplayName)" -ForegroundColor Cyan
}
If -ChangedOnly parameter is not used, then everything is exported:
}else{
#Export all
Out-File -InputObject $CA_JSON -Encoding utf8 -FilePath "$backupDir\$($CA.displayname)\$($ca.id)_$strdate.json"
}
}
The function lists those policies that were deleted:
#Deleted CA
$import_CABackups | ? {$_.id -notin $AAD_CAs.id} | % {
Write-Host "Policy deleted in AzureAD: $($_.Name)" -ForegroundColor Red
}
}
At the end of the script we chose if we call the Backup-AADCAs function with or without -ChangedOnly parameter.
Backup-AADCAs -ChangedOnly
Caveat: if a policy is renamed, a new directory is created. However, since the detection is based on policy ID, no other confusion should occour.