If you know FIM/MIM, you also know that Azure AD Connect is based upon that under the hood. As described in Azure AD Connect sync: Prevent accidental deletes, Azure AD Connect allows you to configure a specific threshold that represents a normal/accepted amount of deletions towards Azure AD. Now, if you have a low amount of objects that you need to investigate you can easily click through the Sync Service Manager. But what happens if you need to investigate hundreds or thousands of pending deletions? Try to do that in the Sync Service Manager and you’ll through a loooooot of pain! Are there easier ways to do that? Fortunately, YES! Therefore keep reading.
–
Now please aware that if the number of deletions is equal to or higher than the deletion threshold it will stop the complete export operation to Azure AD, meaning no adds, updates and deletes to Azure AD! This prevents unintended deletion due to mistakes, bad configurations, etc.
–
To analyze those deletions, use the next steps.
- Logon to the ACTIVE (NON-Staging!) AAD Connect server (To determine the ACTIVE AAD Connect Server, see below!)
- Open a PowerShell Command Prompt Window and export the pending exports from the connector space that needs further analysis (see below)
- Parse the CS Export file to make it readable (see below) (PowerShell GridView Is Opened AND A CSV File Generated!)
- Either use the PowerShell GridView or the CSV to analyze the data being exported!
- For objects being deleted check if those still exist in AD and what the state is (see below)
–
[ad.1] Determine The Active AAD Connect Server
Open a PowerShell Command Prompt Window, and execute:
Import-Module ADSYNC
Get-ADSyncGlobalSettingsParameter | ?{$_.Name -eq "Microsoft.Synchronize.StagingMode"} | Select Name,Value
REMARK: If the VALUE mentions TRUE, then it is the Passive (staging) Server, if the VALUE mentions FALSE or is empty, then it is the Active (Non-Staging) Server
–
[ad.2] Export The Pending Exports From The Connector Space That Needs Analysis
On The Active AAD Connect Server, open a PowerShell Command Prompt Window, and execute:
CD "C:\Program Files\Microsoft Azure AD Sync\Bin"
$connectorHT = New-Object system.collections.hashtable
Write-Host ""
Write-Host "+++ Available Connectors +++" -ForegroundColor Cyan
$connectorNr = 0
Get-ADSyncConnector | %{
$connectorNr++
$connectorName = $null
$connectorName = $_.Name
$connectorHT[$connectorNr.ToString()] = $connectorName
Write-Host "[$connectorNr] – $connectorName" -ForegroundColor Magenta
Write-Host ""
}
$chosenConnectorNr = $null
$chosenConnectorNr = Read-host "Please Choose The Connector By Typing Its Number"
–
$chosenConnectorName = $null
$chosenConnectorName = $connectorHT[$chosenConnectorNr]
$datetime = Get-Date -Format "yyyy-MM-dd_HH.mm.ss"
$csExportXMLFilepath = Join-Path "C:\TEMP" $($datetime + "_CS-" + $chosenConnectorName + "_PendingExports.xml")
$csExportCMD = ".\CSEXPORT.EXE `"$chosenConnectorname`" `"$csExportXMLFilepath`" /f:x"
Invoke-Expression $csExportCMD
Write-Host ""
Write-Host "Export File…….: $csExportXMLFilepath" -ForegroundColor Cyan
Write-Host ""
–
[ad.3] Parse The CS Export XML File
On The Active AAD Connect Server, open a PowerShell Command Prompt Window, and execute:
CD "<Folder With Script>"
$csExportCSVFilepath = $csExportXMLFilepath.TrimEnd(".xml")
.\Parse-CS-Export-XML-To-CSV.ps1 -outToAll -sourceXMLfilePaths $csExportXMLFilepath -targetFilePath $csExportCSVFilepath
REMARK: the GridView will be opened automatically!
Figure 1: Results After Parsing The XML File(s) To A CSV
–
In the GridView or Excel, any value added or deleted, will be specified as such. Unchanged values are not listed
–
Figure 2: GridView Sample Output
–
Figure 3: GridView Sample Output
–
Figure 4: GridView Sample Output
–
Figure 5: GridView Sample Output
–
REMARK: To reopen the GridView using the CSV file use the following command:
Import-CSV $($csExportCSVFilepath + ".csv") | Out-Gridview
or
Import-CSV "<CSV File Path>" | Out-Gridview
–
[ad.5a] Check Deleted USERS Against AD
$csExportCSV = Import-CSV $($csExportCSVFilepath + ".csv")
$objectListUsers = @()
$csExportCSV | ?{$_."Object-Type" -eq "user" -And $_."Ops-Type" -eq "delete"} | %{
$immutableID = $null
$immutableID = $_."Source-ID"
$userPrincipalName = $null
$userPrincipalName = $_."AD-ID"
$ldapFilter = $null
$ldapFilter = "(|(raboADImmutableID=$immutableID)(userPrincipalName=$userPrincipalName))"
$adObject = $null
$adObject = Get-ADObject -LDAPFilter $ldapFilter -Server :3268 -Properties *
$displayName = $null
$status = $null
$canonicalName = $null
If ($adObject) {
$displayName = $adObject.DisplayName
$status = If (($adObject.userAccountControl -band 2) -eq "2") {"Disabled"} Else {"Enabled"}
$canonicalName = $adObject.CanonicalName
} Else {
$displayName = "Unavailable"
$status = "Unavailable"
$canonicalName = "Unavailable"
}
$object = New-Object -TypeName System.Object
$object | Add-Member -MemberType NoteProperty -Name "immutableID" -Value $immutableID
$object | Add-Member -MemberType NoteProperty -Name "userPrincipalName" -Value $userPrincipalName
$object | Add-Member -MemberType NoteProperty -Name "displayName" -Value $displayName
$object | Add-Member -MemberType NoteProperty -Name "status" -Value $status
$object | Add-Member -MemberType NoteProperty -Name "canonicalName" -Value $canonicalName
$objectListUsers += $object
}
$objectListUsers | Out-GridView
REMARK: A Gridview will be opened automatically telling you the status of the object and if it exists in AD
–
[ad.5b] Check Deleted GROUPS Against AD
$objectListGroups = @()
$csExportCSV | ?{$_."Object-Type" -eq "group" -And $_."Ops-Type" -eq "delete"} | %{
$immutableID = $null
$immutableID = $_."Source-ID"
$domain = $null
$domain = $($_."AD-ID").SubString(0, $($_."AD-ID").IndexOf("\"))
$sAMAccountName = $null
$sAMAccountName = $($_."AD-ID").SubString($($_."AD-ID").IndexOf("\") + 1)
$ldapFilter = $null
$ldapFilter = "(|(raboADImmutableID=$immutableID)(sAMAccountName=$sAMAccountName))"
$adObject = $null
$adObject = Get-ADObject -LDAPFilter $ldapFilter -Server $domain`:389 -Properties *
$displayName = $null
$canonicalName = $null
If ($adObject) {
$displayName = $adObject.DisplayName
$canonicalName = $adObject.CanonicalName
} Else {
$displayName = "Unavailable"
$canonicalName = "Unavailable"
}
$object = New-Object -TypeName System.Object
$object | Add-Member -MemberType NoteProperty -Name "immutableID" -Value $immutableID
$object | Add-Member -MemberType NoteProperty -Name "sAMAccountName" -Value $sAMAccountName
$object | Add-Member -MemberType NoteProperty -Name "displayName" -Value $displayName
$object | Add-Member -MemberType NoteProperty -Name "canonicalName" -Value $canonicalName
$objectListGroups += $object
}
$objectListGroups | Out-GridView
REMARK: A Gridview will be opened automatically telling you the status of the object and if it exists in AD
–
[ad.5c] Check Deleted CONTACTS Against AD
Function GuidToEscapedByte($guid) {
$guidParts = $guid.Split("-")
$reverse = $guidParts[0].ToCharArray()[($guidParts[0].Length – 1)..0] + $guidParts[1].ToCharArray()[($guidParts[1].Length – 1)..0] + $guidParts[2].ToCharArray()[($guidParts[2].Length – 1)..0]
$rest = $guidParts[3].ToCharArray() + $guidParts[4].ToCharArray()
for ($inc =0; $inc -lt $reverse.Length; $inc+=2) {
$escapedGUID = $escapedGUID + "\" + $reverse[$inc+1] + $reverse[$inc]
}
for ($inc =0; $inc -lt $rest.Length; $inc+=2) {
$escapedGUID = $escapedGUID + "\" + $rest[$inc] + $rest[$inc+1]
}
return $escapedGUID
}
$csExportCSV = Import-CSV $($csExportCSVFilepath + ".csv")
$objectListContacts = @()
$csExportCSV | ?{$_."Object-Type" -eq "contact" -And $_."Ops-Type" -eq "delete"} | %{
$immutableID = $null
$immutableID = $_."Source-ID"
$objectGUID = $null
$objectGUID = (New-Object -TypeName System.Guid -ArgumentList(,(([System.Convert]::FromBase64String($immutableID))))).Guid
$objectGUIDEscaped = $null
$objectGUIDEscaped = GuidToEscapedByte $objectGUID
$mail = $null
$mail = $_."AD-ID"
$ldapFilter = $null
$ldapFilter = "(|(objectGUID=$objectGUIDEscaped)(mail=$mail))"
$adObject = $null
$adObject = Get-ADObject -LDAPFilter $ldapFilter -Server :3268 -Properties *
$displayName = $null
$canonicalName = $null
If ($adObject) {
$displayName = $adObject.DisplayName
$canonicalName = $adObject.CanonicalName
} Else {
$displayName = "Unavailable"
$canonicalName = "Unavailable"
}
$object = New-Object -TypeName System.Object
$object | Add-Member -MemberType NoteProperty -Name "immutableID" -Value $immutableID
$object | Add-Member -MemberType NoteProperty -Name "mail" -Value $mail
$object | Add-Member -MemberType NoteProperty -Name "displayName" -Value $displayName
$object | Add-Member -MemberType NoteProperty -Name "canonicalName" -Value $canonicalName
$objectListContacts += $object
}
$objectListContacts | Out-GridView
REMARK: A Gridview will be opened automatically telling you the status of the object and if it exists in AD
–
Now assuming you have confirmed all deletions are expected, you can lift the threshold or increase its value (temporarily) to allow the sync cycle to succeed! You need an Azure AD Admin Account with the Global Administrator role
- If needed elevate your account through https://portal.azure.com/ → Privileged Identity Management \ Azure AD Roles \ Global Administrator – Activate
- On the active AAD Connect server, open a PowerShell Command prompt Window and execute:
$aadAdminCreds=Get-Credential
Get-ADSyncExportDeletionThreshold -AADCredential $aadAdminCreds
Disable-ADSyncExportDeletionThreshold -AADCredential $aadAdminCreds
REMARK: The sync engine maybe synching as you do that and you may receive an error. Just wait until the sync engine finishes.
- As soon as the sync engine is not executing a sync cycle, execute:
Start-ADSyncCycle -PolicyType Delta
- As soon as that sync cycle has finished enable the threshold again using the previous value
Enable-ADSyncExportDeletionThreshold -DeletionThreshold <value> -AADCredential $aadAdminCreds
–
PS: this script also works for Pending Export Deletes in FIM/MIM and the script supports multiple source XML files (each for a different CS) as input files!
–
Ohhh, and I almost forgot! You can download the script from here!
–
Cheers,
Jorge
————————————————————————————————————————————————————-
This posting is provided "AS IS" with no warranties and confers no rights!
Always evaluate/test everything yourself first before using/implementing this in production!
This is today’s opinion/technology, it might be different tomorrow and will definitely be different in 10 years!
DISCLAIMER: https://jorgequestforknowledge.wordpress.com/disclaimer/
————————————————————————————————————————————————————-
########################### Jorge’s Quest For Knowledge ##########################
#################### http://JorgeQuestForKnowledge.wordpress.com/ ###################
————————————————————————————————————————————————————-