Backup AzureAD Conditional Access Policies – a different approach

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 window
Connect-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.

Leave a Reply