ADSyncTools.psm1
<#
Disclaimer: The scripts are not supported under any Microsoft standard support program or service. The scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the scripts or documentation, even if Microsoft has been advised of the possibility of such damages. #> #---------------------------------------------------------------------------------------------------------- # # Copyright © 2024 Microsoft Corporation. All rights reserved. # #---------------------------------------------------------------------------------------------------------- # # NAME: Azure AD Connect ADSyncTools PowerShell Module # #---------------------------------------------------------------------------------------------------------- # # RELEASE NOTES # # Version 1.5.0 - 2024-10-04 # - Introduced Microsoft Graph PowerShell SDK base functions # - Introduced new functions for managing onpremises attributes using Microsoft Graph # Get/Set/Clear-ADSyncToolsOnPremisesAttribute # # Version 1.5.1 - 2024-10-07 # - Get/Set/Clear-ADSyncToolsOnPremisesAttribute: Improved support for pipelining cmdlets # # Version 1.5.2 - 2024-10-24 # - Excluded onPremisesImmutableId from Clear-ADSyncToolsOnPremisesAttribute -All # #---------------------------------------------------------------------------------------------------------- #======================================================================================= #region Internal Variables #======================================================================================= [string[]] $defaultMsolObjProperties = @('DisplayName', 'ObjectId', 'isLicensed', 'LastDirSyncTime', 'ImmutableId', 'UserPrincipalName', 'MobilePhone', 'AlternateMobilePhones') [string[]] $defaultAADobjProperties = @('ProxyAddresses', 'SourceAnchor', 'DisplayName', 'Mail') [string[]] $defaultADobjProperties = @('DistinguishedName', 'ObjectClass', 'Name', 'ObjectGUID', 'ObjectSID', 'mS-DS-ConsistencyGuid', 'sAMAccountName', 'CanonicalName', 'msDS-PrincipalName', 'UserPrincipalName') [string[]] $defaultGraphOnPremisesProperties = @('onPremisesDistinguishedName', 'onPremisesDomainName', 'onPremisesImmutableId', 'onPremisesSamAccountName', 'onPremisesSecurityIdentifier', 'onPremisesUserPrincipalName') [string] $msGraphInstallMsg = "See https://learn.microsoft.com/en-us/powershell/microsoftgraph/installation on how to install Microsoft Graph SDK module. " [string] $adSyncInstallMsg = "Microsoft Entra Connect Sync needs to be installed and running. " [regex] $distinguishedNameRegex = '^(?:(?<cn>CN=(?<name>[^,]*)),)?(?:(?<path>(?:(?:CN|OU)=[^,]+,?)+),)?(?<domain>(?:DC=[^,]+,?)+)$' [regex] $upnRegex = "^[a-zA-Z0-9.!£#$%&'^_`{}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" [regex] $guidRegex = '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}' [regex] $netbiosDomainRegex = "w*\\\w+" #endregion #======================================================================================= #======================================================================================= #region class definitions #======================================================================================= class DuplicateUserSourceAnchorInfo { [string] $UserName [string] $DistinguishedName [string] $ADDomainName [Byte[]] $CurrentMsDsConsistencyGuid [Byte[]] $ExpectedMsDsConsistencyGuid } #endregion #======================================================================================= #======================================================================================= #region Internal Functions #======================================================================================= <# .SYNOPSIS Checks if <ModuleName> PowerShell module is present and imports it #> Function Import-ADSyncToolsModule { [CmdletBinding()] Param ( [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [string] $ModuleName, [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=1)] [string] $InstallMessage, [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=2)] [switch] $CheckOnly ) if ($CheckOnly) { # Check if module is available If (-not (Get-Module $ModuleName -ListAvailable)) { Write-Warning "$ModuleName PowerShell module is required for some functions to work. ADSyncTools will run with limited functionality.`n$InstallMessage" } } else { # Import Module If (-not (Get-Module $ModuleName)) { Try { # Import powershell module Import-Module $ModuleName -ErrorAction Stop } Catch { Throw "Unable to import $ModuleName PowerShell module. $InstallMessage Error Details: $($_.Exception.Message)" } } } } Function Import-ADSyncToolsModule { [CmdletBinding()] Param ( [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [string] $ModuleName, [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=1)] [string] $InstallMessage, [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=2)] [switch] $CheckOnly ) if ($CheckOnly) { # Check if module is available If (-not (Get-Module $ModuleName -ListAvailable)) { Write-Warning "$ModuleName PowerShell module is required for some functions to work. ADSyncTools will run with limited functionality.`n$InstallMessage" } } else { # Import Module If (-not (Get-Module $ModuleName)) { Try { # Import powershell module Import-Module $ModuleName -ErrorAction Stop } Catch { Throw "Unable to import $ModuleName PowerShell module. $($InstallMessage)Error Details: $($_.Exception.Message)" } } } } <# .SYNOPSIS Checks if AADConnector PowerShell Module is present and imports it #> Function Import-ADSyncToolsAADConnectorBinaries { [CmdletBinding()] Param () $binariesPath = Get-ADSyncToolsADsyncFolder If ($binariesPath -eq '') { Write-Warning "Azure AD Connect installation was not found, some functionality may be unavailable. Using current directory '$PSScriptRoot'." $binariesPath = $PSScriptRoot } Write-Verbose "Importing binaries from '$binariesPath'..." Try { Import-Module $(Join-Path -Path $binariesPath -ChildPath "Bin\ADSync\ADSync.psd1") Add-Type -Path $(Join-Path -Path $binariesPath -ChildPath "Bin\ADSync\Microsoft.Online.Coexistence.Schema.Ex.dll") Add-Type -Path $(Join-Path -Path $binariesPath -ChildPath "Bin\Assemblies\Microsoft.MetadirectoryServicesEx.dll") Add-Type -Path $(Join-Path -Path $binariesPath -ChildPath "Bin\Microsoft.MetadirectoryServices.PasswordHashSynchronization.Types.dll") Add-Type -Path $(Join-Path -Path $binariesPath -ChildPath "Extensions\Microsoft.Azure.ActiveDirectory.Connector.dll") } Catch { Throw "Unable to import AADConnector binaries. Error Details: $($_.Exception.Message)" } } <# .SYNOPSIS Gets Azure AD Connect installed folder location from the registry. To be used with [string]::IsNullOrEmpty($(Get-ADSyncToolsADsyncFolder)) #> Function Get-ADSyncToolsADsyncFolder { [CmdletBinding()] Param () $path = '' $paramsRegKey = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ADSync\Parameters\' Try { Write-verbose "Reading AADConnect config from registry..." $adSyncReg = Get-ItemProperty -Path Registry::$paramsRegKey -ErrorAction Stop } Catch { Write-Verbose "Azure AD Connect path not found. Error Details: $($_.Exception.Message)" } If ($adSyncReg -ne $null) { $path = $adSyncReg.Path } # Returns the absolute path or an empty string if AADConnect is not found. Return $path } <# .SYNOPSIS Checks if Microsoft Graph is connected #> Function Confirm-ADSyncToolsGraphConnection { [CmdletBinding()] Param( [Parameter(Mandatory = $true, Position = 0)] [String] $RequiredScope ) Import-ADSyncToolsModule -ModuleName Microsoft.Graph.Authentication -InstallMessage $msGraphInstallMsg $session = Get-MgContext Write-Verbose "Scopes: $($session.Scopes -join ',')" if ($null -eq $session) { # TODO: limit the Scopes to specific tools/scenarios Throw "Microsoft Graph authentication needed. Please call: Connect-MgGraph -Scopes '$RequiredScope'" } else { # Optimizatoin Note: make scope evaluation smarter (resource/operation/contraint) # I.e., if Scopes already contains "User.ReadWrite.All", then a requesting a scope of "User.Read.Create", shouldn't ask to Connect Graph again for this specific scope. # More information: https://learn.microsoft.com/en-us/graph/permissions-overview $reqScopesList = $RequiredScope.Replace(" ","") -split ',' ForEach ($scope in $reqScopesList) { If ($session.Scopes -notcontains $scope) { Throw "Microsoft Graph authentication needed. Please call: `nConnect-MgGraph -Scopes '$RequiredScope'" } Write-Verbose "$scope scope is present." } } } <# .SYNOPSIS Checks if Azure AD Connect is present and if it have a min/max version. #> Function IsAADConnectPresent { [CmdletBinding()] Param ( [Parameter(Mandatory = $false, HelpMessage = 'Minimum accepted version', Position = 0)] [System.Version] $MinVersion, [Parameter(Mandatory = $false, HelpMessage = 'Maximum accepted version', Position = 0)] [System.Version] $MaxVersion ) $adSyncFolder = Get-ADSyncToolsADsyncFolder If ([string]::IsNullOrEmpty($adSyncFolder)) { Throw "Entra Connect installation not found." } [string] $miiserverPath = $adSyncFolder + 'Bin\miiserver.exe' Write-Verbose "Miiserver.exe absolute path is '$miiserverPath'" Try { $miiserver = Get-ItemProperty -Path $miiserverPath -ErrorAction Stop [System.Version] $miiserverVersion = $miiserver.VersionInfo.FileVersion } Catch { Throw "Azure AD Connect version not found. Error Details: $($_.Exception.Message)" } Write-Verbose "Current Azure AD Connect version is '$miiserverVersion'" If (-not [string]::IsNullOrEmpty($MinVersion)) { Write-Verbose "MinVersion: $MinVersion - Check if current version ($miiserverVersion) >= version $MinVersion" If (-not ($miiserverVersion -ge $MinVersion)) { Throw "Function not supported in Azure AD Connect version '$miiserverVersion'. Minimum version to run this function is '$MinVersion'." } } ElseIf (-not [string]::IsNullOrEmpty($MaxVersion)) { Write-Verbose "MaxVersion: $MaxVersion - Check if current version ($miiserverVersion) <= $MaxVersion version" If (-not ($miiserverVersion -le $MaxVersion)) { Throw "Function not supported in Azure AD Connect version '$miiserverVersion'. Oldest version to run this function is '$MaxVersion'." } } Else { If (-not ($miiserverVersion -gt "1.0.0.0")) { Throw "Function not supported in Azure AD Connect version '$miiserverVersion'. Minimum version to run this function is '1.0.0.0'." } } } <# .SYNOPSIS Checks for PowerShell version 7 #> Function Confirm-ADSyncToolsPowerShellV7 { $version = $PSVersionTable.PSVersion if ($version.Major -lt 7) { Write-Warning "PowerShell version 7 or later is required for some functions to work. Please upgrade your PowerShell version." } } <# .SYNOPSIS Checks if PowerShell session is running with Administrator privileges #> Function IsPowerShellSessionElevated { [CmdletBinding()] Param () If (([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent() ` ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -eq $false) { Throw "To use this function you need Administrator privileges. Please start PowerShell with 'Run As Administrator'." } } Function InstallModuleDepedency { [CmdletBinding()] Param ( # UserprincipalName [Parameter(Mandatory=$True, Position=0)] [string] $ModuleName ) $module = Get-InstalledModule $ModuleName -ErrorAction Ignore If ($null -eq $module) { Write-Host "Installing '$ModuleName' Module. Please wait..." -ForegroundColor Cyan Try { Install-Module $ModuleName -Force -ErrorAction Stop } Catch { Throw "There was a problem installing '$ModuleName' Module. Error Details: $($_.Exception.Message)" } } } <# .SYNOPSIS Installs all PowerShell depedencies #> Function Install-ADSyncToolsPrerequisites { [CmdletBinding()] Param () IsPowerShellSessionElevated # PowerShellGet Module $powerShellGetModule = @(Get-Module PowerShellGet -ListAvailable) $powerShellGetInstalled = $false [System.Version] $minVersion = "2.2.4.1" ForEach ($m in $powerShellGetModule) { Write-Verbose "PowerShellGet current version: $($m.Version) | PowerShellGet minimum version: $minVersion" If ($m.Version -ge $minVersion) { $powerShellGetInstalled = $true Write-Verbose "PowerShellGet module is already installed." } } If (-not $powerShellGetInstalled) { Write-Host "Installing 'PowerShellGet' Module. Please wait..." -ForegroundColor Cyan Try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module PowerShellGet -Force -ErrorAction Stop } Catch { Throw "There was a problem installing 'PowerShellGet' Module. Error Details: $($_.Exception.Message)" } } # RSAT Tools Try { $winFeature = Get-WindowsFeature RSAT-AD-Tools } Catch { Throw "There was a problem checking Windows Features. Error Details: $($_.Exception.Message)" } If (-not $winFeature.Installed) { Write-Host "Installing Windows Feature 'RSAT-AD-Tools'. Please wait..." -ForegroundColor Cyan Try { Install-WindowsFeature RSAT-AD-Tools -ErrorAction Stop } Catch { Throw "There was a problem installing Windows Feature 'RSAT-AD-Tools'. Error Details: $($_.Exception.Message)" } } # MSOnline module InstallModuleDepedency -ModuleName MSOnline # AzureAD module InstallModuleDepedency -ModuleName AzureAD } <# .SYNOPSIS Connects ADSyncTools Module to Azure AD and Exchange Online #> Function Connect-ADSyncTools { [CmdletBinding()] Param ( [Parameter(Mandatory = $false, #ParameterSetName = 'Username', HelpMessage = 'Enter Azure AD Global Administrator username', Position = 0)] [String] $UserName, [Parameter(Mandatory = $false, ParameterSetName = 'PSCredential', HelpMessage = 'Enter Azure AD Global Administrator credential', Position = 0)] [PSCredential] $Credential ) If (-not [string]::IsNullOrEmpty($UserName)) { $UserCredential = Get-Credential -UserName $UserName -Message 'Global Administrator sign-in:' } ElseIf ($Credential) { $UserCredential = $Credential } Else { Write-Verbose "No UserName, no Credential, prompting for Credentials..." $UserCredential = Get-Credential -Message 'Global Administrator sign-in:' } If ($null -eq $UserCredential) { Write-Warning "Global Administrator credential not provided, you'll be prompted multiple times for authentication. Do you want to continue?" -WarningAction Inquire } Write-Host "`nConnecting to Exchange Online..." -ForegroundColor Cyan $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $UserCredential -Authentication Basic -AllowRedirection Import-PSSession $Session -DisableNameChecking Write-Host "`nConnecting MSOnline Module..." -ForegroundColor Cyan Import-Module MSOnline Connect-MsolService -Credential $UserCredential Write-Host "`nConnecting AzureAD Module..." -ForegroundColor Cyan Import-Module AzureAD Connect-AzureAD -Credential $UserCredential } <# .SYNOPSIS Generates a new file name path based on current time #> Function Get-ADsyncToolsLogFilename { [CmdletBinding()] Param ( # Name [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [string] $Name, # Extension [Parameter(Mandatory=$false, Position=1)] [string] $Extension = 'log' ) # Generate a new filename $prefix = "ADSyncTools-$Name" $currentTime = Get-Date -Format yyyyMMdd-HHmmss $location = Get-Location $filename = Join-Path -Path $location -ChildPath "$($prefix)_$currentTime.$Extension" Return $filename } <# .SYNOPSIS Gets the tenant name from a user's UPN suffix #> Function Get-ADSyncToolsUPNsuffix { [CmdletBinding()] Param ( # UserprincipalName [Parameter(Mandatory=$True, Position=0)] [string] $UserPrincipalName ) # Get tenant name from user's upn suffix $tenant = $UserPrincipalName.Split('@')[1] If ([string]::IsNullOrEmpty($tenant)) { Throw "Invalid Azure AD user name. Please provide the user name in UserPrincipalName format (user@contoso.com)." } Return $tenant } <# .SYNOPSIS Helper function to get which Azure environment the user belongs. .DESCRIPTION This function will call Oauth discovery endpoint to get CloudInstance and tenant_region_scope to determine the Azure environment. https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration cloud_instance_name azure_environment =================== ================= microsoftonline.de AzureGermanyCloud chinacloudapi.cn AzureChinaCloud microsoftonline.com AzureCloud/USGovernment tenant_region_scope azure_environment =================== ================= USG USGovernment .EXAMPLE Get-ADSyncToolsTenantAzureEnvironment -Credential (Get-Credential) .INPUTS The user's PS Credential object .OUTPUTS The Azure environment (string) #> Function Get-ADSyncToolsTenantAzureEnvironment { [CmdletBinding()] Param ( # The user's PS Credential object [Parameter(Mandatory=$True, Position=0)] [System.Management.Automation.PSCredential] $Credential ) # Set default Azure environment to public $environment = "AzureCloud" # Get tenant name from user's upn suffix $tenant = Get-ADSyncToolsUPNsuffix -UserPrincipalName $Credential.UserName # Oauth discovery endpoint $url = "https://login.microsoftonline.com/$tenant/.well-known/openid-configuration" # Get CloudInstance from Oauth discovery endpoint try { $response = Invoke-RestMethod -Uri $url -Method Get } catch [Exception] { # We failed, but that is possible if the tenant is in PPE. So check against PPE before failing. try { # Oauth discovery endpoint for PPE $url = "https://login.windows-ppe.net/$tenant/.well-known/openid-configuration" $response = Invoke-RestMethod -Uri $url -Method Get } catch [Exception] { Write-Output "$_.Exception.Message" Write-Output "[ERROR]`t OAuth2 discovery failed. Please contact system administrator for more information." break } } # Determine AzureEnvironment from tenant_region_scope and cloud_instance_name if ($response.tenant_region_scope.ToLower().equals("usg")) { $environment = "USGovernment" } elseif ($response.cloud_instance_name.ToLower().equals("chinacloudapi.cn")) { $environment = "AzureChinaCloud" } elseif ($response.cloud_instance_name.ToLower().equals("microsoftonline.de")) { $environment = "AzureGermanyCloud" } elseif ($response.cloud_instance_name.ToLower().equals("windows-ppe.net")) { $environment = "AzurePPE" } return $environment } <# .SYNOPSIS Encodes reserved characters in DistinguishedName to Hexadecimal values based on MSDN documentation: The following table lists reserved characters that cannot be used in an attribute value without being escaped. From: https://msdn.microsoft.com/en-us/windows/desktop/aa366101 #> Function Format-ADSyncToolsDistinguishedName { [CmdletBinding()] Param ( [Parameter(Mandatory=$True, Position=0)] [String] $DistinguishedName ) $escapedDN = $DistinguishedName -replace '\\#','\23' ` -replace '\\,','\2C' ` -replace '\\"','\22' ` -replace '\\<','\3C' ` -replace '\\>','\3E' ` -replace '\\;','\3B' ` -replace '\\=','\3D' ` -replace '\\/','\2F' Return $escapedDN } <# .Synopsis Gets a domain controller in the Forest for a given DistinguishedName. .DESCRIPTION Returns one target Domain Controller #> Function Get-ADSyncToolsADtargetDC { [CmdletBinding()] Param ( # Domain Controller type [Parameter(Mandatory = $true, Position = 0)] [ValidateSet('GlobalCatalog', 'Readable', 'Writable')] $Service, # Target AD Domain [Parameter(Mandatory=$false, Position=1)] [ValidateNotNullOrEmpty()] $DomainName ) If ($Service -ne 'GlobalCatalog' -and ($DomainName -eq "" -or $DomainName -eq $null)) { Throw "A DomainName in FQDN format must be provided to get a $Service Domain Controller" } Import-ADSyncToolsActiveDirectoryModule switch ($Service) { 'GlobalCatalog' { # Find a target Global Catalog Domain Controller Try { [string] $domainCtrlName = (Get-ADDomainController -Discover -Service GlobalCatalog -ErrorAction Stop).HostName | select -First 1 Write-Verbose "Target DC (Global Catalog): $domainCtrlName" } Catch { Throw "Cannot find a $Service Domain Controller: $($_.Exception.Message)" } } 'Readable' { # Find a target Readable Domain Controller for AD domain Try { [string] $domainCtrlName = (Get-ADDomainController -Discover -DomainName $DomainName -ErrorAction Stop).HostName | select -First 1 Write-Verbose "Target DC for Domain '$DomainName': $domainCtrlName" } Catch { Throw "Cannot find a $Service Domain Controller: $($_.Exception.Message)" } } 'Writable' { # Find a target Writable Domain Controller for AD domain Try { [string] $domainCtrlName = (Get-ADDomainController -Discover -DomainName $DomainName -Writable -ErrorAction Stop).HostName | select -First 1 Write-Verbose "Target DC for Domain '$DomainName': $domainCtrlName" } Catch { Throw "Cannot find a $Service Domain Controller: $($_.Exception.Message)" } } } If ($domainCtrlName -eq "" -or $domainCtrlName -eq $null) { Throw "Unable to find a Domain Controller." } Return $domainCtrlName } <# .Synopsis Get Active Directory Domain DistinguishedName .DESCRIPTION Returns the DistinguishedName of the Active Directory Domain for a given Active Directory object. #> Function Get-ADSyncToolsDomainDN { [CmdletBinding()] Param ( # Target User in AD to set ConsistencyGuid [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] $DistinguishedName ) # Get the Domain portion of the object's DN Try { $domainDN = $DistinguishedName.Substring($DistinguishedName.IndexOf('DC=')) Write-Verbose "Object's Domain DN: $domainDN" } Catch { Throw "DistinguishedName '$DistinguishedName' is invalid." } Return $domainDN } <# .Synopsis Get Active Directory Domain FQDN from DistinguishedName .DESCRIPTION Returns the respective FQDN of the Active Directory Domain for a given Active Directory object. #> Function Get-ADSyncToolsDomainDns { [CmdletBinding()] Param ( # Target User in AD to set ConsistencyGuid [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] $DistinguishedName ) Import-ADSyncToolsActiveDirectoryModule $domainDN = Get-ADSyncToolsDomainDN $DistinguishedName # Get the Domains in the Forest $domains = @((Get-ADForest).Domains | %{Get-ADDomain -Identity $_}) Write-Verbose "Domains in AD Forest: $($domains | Select -ExpandProperty DistinguishedName)" # Select the object's domain $domain = $domains | Where-Object {$_.DistinguishedName -eq $domainDN} Write-Verbose "Domain FQDN: $($domain.DNSRoot)" If ($domain -eq $null) { Throw "Cannot find Domain for object '$DistinguishedName'." } Return $domain.DNSRoot } #endregion #======================================================================================= #======================================================================================= #region Troubleshooting Functions #======================================================================================= <# .Synopsis Search an Active Directory object in Active Directory Forest by its UserPrincipalName, sAMAccountName or DistinguishedName .DESCRIPTION Supports multi-domain queries and returns all the required properties including mS-DS-ConsistencyGuid. .EXAMPLE Search-ADSyncToolsADobject 'CN=user1,OU=Sync,DC=Contoso,DC=com' .EXAMPLE Search-ADSyncToolsADobject -Identity "user1@Contoso.com" .EXAMPLE Get-ADUser 'CN=user1,OU=Sync,DC=Contoso,DC=com' | Search-ADSyncToolsADobject #> Function Search-ADSyncToolsADobject { [CmdletBinding()] Param ( # Target User identity to search in AD [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity, # Properties to retrieve from AD [Parameter(Mandatory=$false, Position=1)] [string[]] $Properties = @() ) Import-ADSyncToolsActiveDirectoryModule [string[]] $Properties = $defaultADobjProperties + @($Properties) Try { If ($Identity.GetType() -like "Microsoft.ActiveDirectory.Management*") { # User input is AD object, but it might not contain mS-DS-ConsistencyGuid property Write-Verbose "Identity input is an AD object - Getting AD user '$Identity' with required properties" $seachResult = Search-ADSyncToolsADobjectByDN $Identity.DistinguishedName -Properties $Properties } Else { If ($Identity -match $upnRegex) { # User input is in UPN format Write-Verbose "Identity input is an UserPrincipalName - Getting AD user '$Identity' with required properties '$Properties'" $seachResult = Search-ADSyncToolsADobjectByUPN -UserPrincipalName $Identity -Properties $Properties } Else { $escapedDN = Format-ADSyncToolsDistinguishedName -DistinguishedName $Identity If ($escapedDN -match $distinguishedNameRegex) { # User input is in DistinguishedName format Write-Verbose "Identity input is an DistinguishedName - Getting AD user '$Identity' with required properties" $seachResult = Search-ADSyncToolsADobjectByDN -DistinguishedName $Identity -Properties $Properties } Else { # Unknown format, try to seach on sAMAccountName on local domain Write-Verbose "Identity input is a string - Searching for sAMAccountName '$Identity' on current AD Domain only" $seachResult = Get-ADObject -Filter 'sAMAccountName -eq $Identity' -Properties $Properties -ErrorAction Stop Write-Warning "Searching for sAMAccountName '$Identity' is limited the current AD Domain only. Please use a DistinguishedName or UserPrincipalName to search for objects across the entire AD Forest." } } } } Catch { Throw "Unable to search in Active Directory: $($_.Exception.Message)" } If ($seachResult) { # Create Custom Object to hold all the required properties $result = $seachResult | Select $Properties <# $result.Name = $seachResult.Name $result.ObjectClass = $seachResult.ObjectClass $result.DistinguishedName = $seachResult.DistinguishedName $result.ObjectGUID = $seachResult.ObjectGUID $result.ObjectSID = $seachResult.ObjectSID $result.sAMAccountName = $seachResult.sAMAccountName $result.UserPrincipalName = $seachResult.UserPrincipalName #> # Add mS-DS-ConsistencyGuid in Guid-string format If ($null -ne $seachResult.'mS-DS-ConsistencyGuid') { Try { $result.'mS-DS-ConsistencyGuid' = [Guid] $seachResult.'mS-DS-ConsistencyGuid' } Catch { Write-Error "Unable to convert mS-DS-ConsistencyGuid to GUID: $($_.Exception.Message)" } } Else { Write-Verbose "Object '$($result.Name)' does not have a mS-DS-ConsistencyGuid value" } } Else { Throw "Unable to find object in Active Directory." } Return $result } <# .Synopsis Find an Active Directory object in the Forest by its DistinguishedName. .DESCRIPTION Supports multi-domain queries and returns all the required properties including mS-DS-ConsistencyGuid. DistinguishedName value must be validated by the caller #> Function Search-ADSyncToolsADobjectByDN { [CmdletBinding()] Param ( # Target DistinguishedName to search [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $DistinguishedName, # Properties to retrieve from AD [Parameter(Mandatory=$true, Position=1)] $Properties ) Import-ADSyncToolsActiveDirectoryModule $domainDNS = Get-ADSyncToolsDomainDns -DistinguishedName $DistinguishedName $targetDC = Get-ADSyncToolsADtargetDC -Service Readable -DomainName $domainDNS $domainDN = Get-ADSyncToolsDomainDN -DistinguishedName $DistinguishedName # Get the AD object from target DC Write-Verbose "Executing: Get-ADObject -Filter `"distinguishedName -eq '$DistinguishedName'`" -Properties $Properties -SearchBase $domainDN -SearchScope Subtree -Server $targetDC" Try { $seachResult = Get-ADObject -Filter "distinguishedName -eq '$DistinguishedName'" -Properties $Properties -SearchBase $domainDN -SearchScope Subtree -Server $targetDC -ErrorAction Stop } Catch { Throw "Cannot find user '$DistinguishedName': $($_.Exception.Message)" } Return $seachResult } <# .Synopsis Find an Active Directory object in the Forest by its UserPrincipalName. .DESCRIPTION Supports multi-domain queries and returns all the required properties including mS-DS-ConsistencyGuid. #> Function Search-ADSyncToolsADobjectByUPN { [CmdletBinding()] Param ( # Target UserPrincipalName to search [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $UserPrincipalName, # Properties to retrieve from AD [Parameter(Mandatory=$true, Position=1)] [string[]] $Properties ) Import-ADSyncToolsActiveDirectoryModule $targetDC = Get-ADSyncToolsADtargetDC -Service GlobalCatalog # Get the AD object from target DC Write-Verbose "Executing: Get-ADObject -Filter `"UserPrincipalName -eq '$UserPrincipalName'`" -Properties `"$Properties`" -Server `"$($targetDC):3268`"" Try { $globalCatalogObj = Get-ADObject -Filter "UserPrincipalName -eq '$UserPrincipalName'" -Properties $Properties -Server "$($targetDC):3268" -ErrorAction Stop } Catch { Throw "Cannot find user '$UserPrincipalName': $($_.Exception.Message)" } # Get all the required properties of the object including mS-DS-ConsistencyGuid If ($globalCatalogObj) { $seachResult = Search-ADSyncToolsADobjectByDN $globalCatalogObj.DistinguishedName -Properties $Properties } Return $seachResult } <# .Synopsis Sets an object's attribute in Active Directory Forest .DESCRIPTION Supports multi-domain queries .EXAMPLE Set-ADSyncToolsADobject -ADObject <ADObject> -AttributeName 'Mobile' -AttributeValue '09998887' .EXAMPLE Set-ADSyncToolsADobject -ADObject <ADObject> -OtherMobile '0987654','1234567' -Server DC1.Contoso.com -Credential <PSCredential> #> Function Set-ADSyncToolsADobject { [CmdletBinding()] Param ( # Target object in AD to update [Parameter(Mandatory=$true, Position=0)] $ADObject, # Attribute Name [Parameter(Mandatory=$true, Position=1)] [string] $AttributeName, # Attribute Value [Parameter(Mandatory=$true, Position=2)] $AttributeValue, # Credential for target AD Domain [Parameter(Mandatory=$false, Position=3)] [pscredential] $Credential, # Server [Parameter(Mandatory=$false, Position=4)] [string] $Server ) $oldValue = $ADObject.$AttributeName If ([string]::IsNullOrEmpty($Server)) { # Get the target Writable DC $domainDNS = Get-ADSyncToolsDomainDns -DistinguishedName $ADObject.DistinguishedName $targetDC = Get-ADSyncToolsADtargetDC -Service Writable -DomainName $domainDNS } Else { $targetDC = $Server } Try { # Set new value on target object in AD #If ($Credential.UserName -eq 'ADCredentialNotProvided') If ($null -eq $Credential) { Set-ADObject -Identity $ADObject.DistinguishedName -Replace @{$AttributeName=$AttributeValue} -Server $targetDC } Else { Write-Verbose "Using Credential '$($Credential.UserName)'" Set-ADObject -Identity $ADObject.DistinguishedName -Replace @{$AttributeName=$AttributeValue} -Server $targetDC -Credential $Credential } Write-Verbose "Attribute '$AttributeName' updated from '$oldValue' to '$AttributeValue' in '$($ADObject.DistinguishedName)' object successfully" } Catch { # Unable to update user Throw "Unable to update '$AttributeName' with '$AttributeValue' in '$($ADObject.DistinguishedName)' object: $($_.Exception.Message)" } } <# .Synopsis Clears an object's attribute in Active Directory Forest .DESCRIPTION Supports multi-domain queries .EXAMPLE Clear-ADSyncToolsADobject -ADObject <ADObject> -AttributeName 'Mobile' .EXAMPLE Clear-ADSyncToolsADobject -ADObject <ADObject> -OtherMobile -Server DC1.Contoso.com -Credential <PSCredential> #> Function Clear-ADSyncToolsADobject { [CmdletBinding()] Param ( # Target object in AD to update [Parameter(Mandatory=$true, Position=0)] $ADObject, # Attribute Name [Parameter(Mandatory=$true, Position=1)] [string] $AttributeName, # Credential for target AD Domain [Parameter(Mandatory=$false, Position=2)] [pscredential] $Credential, # Server [Parameter(Mandatory=$false, Position=3)] [string] $Server ) $oldValue = $ADObject.$AttributeName # Get target DC for updating user If ([string]::IsNullOrEmpty($Server)) { # Get the target Writable DC $domainDNS = Get-ADSyncToolsDomainDns -DistinguishedName $ADObject.DistinguishedName $targetDC = Get-ADSyncToolsADtargetDC -Service Writable -DomainName $domainDNS } Else { $targetDC = $Server } Write-Verbose "Using target DC: '$targetDC'" Try { # Set new value on target object in AD #If ($Credential.UserName -eq 'ADCredentialNotProvided') If ($null -eq $Credential) { Write-Verbose "No credential provided, using current user." Set-ADObject -Identity $ADObject.DistinguishedName -Clear $AttributeName -Server $targetDC } Else { Write-Verbose "Using Credential '$($Credential.UserName)'" Set-ADObject -Identity $ADObject.DistinguishedName -Clear $AttributeName -Server $targetDC -Credential $Credential } Write-Verbose "Attribute '$AttributeName' cleared in '$($ADObject.DistinguishedName)' object successfully" } Catch { # Unable to update user Throw "Unable to clear '$AttributeName' in '$($ADObject.DistinguishedName)' object: $($_.Exception.Message)" } } <# .Synopsis Get an Active Directory object ms-ds-ConsistencyGuid .DESCRIPTION Returns the value in mS-DS-ConsistencyGuid attribute of the target Active Directory object in GUID format. Supports Active Directory objects in multi-domain forests. .EXAMPLE Get-ADSyncToolsMsDsConsistencyGuid -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' .EXAMPLE Get-ADSyncToolsMsDsConsistencyGuid -Identity 'User1@Contoso.com' .EXAMPLE 'User1@Contoso.com' | Get-ADSyncToolsMsDsConsistencyGuid #> Function Get-ADSyncToolsMsDsConsistencyGuid { [CmdletBinding()] Param ( # Target object in AD to get [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity ) # Get object from AD $adObject = Search-ADSyncToolsADobject -Identity $Identity # Get mS-DS-ConsistencyGuid value Write-Verbose "Object '$($adObject.Name)' | mS-DS-ConsistencyGuid: $($adObject.'mS-DS-ConsistencyGuid')" Return $adObject.'mS-DS-ConsistencyGuid' } <# .Synopsis Set an Active Directory object ms-ds-ConsistencyGuid .DESCRIPTION Sets a value in mS-DS-ConsistencyGuid attribute for the target Active Directory user. Supports Active Directory objects in multi-domain forests. .EXAMPLE Set-ADSyncToolsMsDsConsistencyGuid -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' -Value '88666888-0101-1111-bbbb-1234567890ab' .EXAMPLE Set-ADSyncToolsMsDsConsistencyGuid -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' -Value 'GGhsjYwBEU+buBsE4sqhtg==' .EXAMPLE Set-ADSyncToolsMsDsConsistencyGuid 'User1@Contoso.com' '8d6c6818-018c-4f11-9bb8-1b04e2caa1b6' .EXAMPLE Set-ADSyncToolsMsDsConsistencyGuid 'User1@Contoso.com' 'GGhsjYwBEU+buBsE4sqhtg==' .EXAMPLE '88666888-0101-1111-bbbb-1234567890ab' | Set-ADSyncToolsMsDsConsistencyGuid -Identity User1 .EXAMPLE 'GGhsjYwBEU+buBsE4sqhtg==' | Set-ADSyncToolsMsDsConsistencyGuid User1 #> Function Set-ADSyncToolsMsDsConsistencyGuid { [CmdletBinding()] Param ( # Target object in AD to set mS-DS-ConsistencyGuid [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity, # Value to set (ImmutableId, Byte array, GUID, GUID string or Base64 string) [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] $Value, # Credential to set target object [Parameter(Mandatory=$false, Position=2)] [pscredential] $Credential ) # Parse ConsistencyGuid value to set Switch ($Value.GetType().Name) { 'Guid' # Value is a GUID { $keepTrying = $false $targetValue = $Value } 'String' # Value is a string, either a GUID string or a Based64 encoded GUID { $keepTrying = $false Try { # Try to decode from Base64 $targetValueDecoded = [system.convert]::FromBase64String($Value) Write-Verbose "Value converted from Base64 string" } Catch { # Could not convert from Base64 $keepTrying = $true Write-Verbose "Value cannot be converted from Base64 string" } if (-not $keepTrying) { # Value decoded from Base64 successfully Try { # Try to convert to GUID $targetValue = [GUID] $targetValueDecoded Write-Verbose "Value converted from decoded Base64 string" } Catch { # Fatal, could not convert Base64 to GUID Throw "$Value is not recognized as a valid GUID value: $($_.Exception.Message)" } } } Default { $keepTrying = $true } } # Continue parsing ConsistencyGuid value If ($keepTrying) { # Still not a GUID value Write-Verbose "Still trying to convert Value to GUID. - Value Type = $($Value.getType())" Try { # Try to convert to GUID directy $targetValue = [GUID] $Value Write-Verbose "Converted mS-DS-ConsistencyGuid value successfully: $targetValue" } Catch { # Fatal, could not convert from Base64 Throw "'$Value' is not recognized as a valid GUID: $($_.Exception.Message)" } } # Get the target object from AD $adObject = Search-ADSyncToolsADobject -Identity $Identity Write-Verbose "Found Object '$($adObject.Name)' | mS-DS-ConsistencyGuid: $($adObject.'mS-DS-ConsistencyGuid')" # Get the target Writable DC $domainDNS = Get-ADSyncToolsDomainDns -DistinguishedName $adObject.DistinguishedName $targetDC = Get-ADSyncToolsADtargetDC -Service Writable -DomainName $domainDNS Try { # Set the target mS-DS-ConsistencyGuid If ($Credential) { Set-ADObject -Identity $adObject.DistinguishedName -Replace @{'mS-DS-ConsistencyGuid'=$targetValue} -Server $targetDC -Credential $Credential } Else { Set-ADObject -Identity $adObject.DistinguishedName -Replace @{'mS-DS-ConsistencyGuid'=$targetValue} -Server $targetDC } Write-Verbose "New mS-DS-ConsistencyGuid set: $targetValue" } Catch { # Fatal, could not set user Throw "Unable to set mS-DS-ConsistencyGuid on '$($adObject.Name)': $($_.Exception.Message)" } } <# .Synopsis Clear an Active Directory object mS-DS-ConsistencyGuid .DESCRIPTION Clears the value in mS-DS-ConsistencyGuid for the target Active Directory object. Supports Active Directory objects in multi-domain forests. .EXAMPLE Clear-ADSyncToolsMsDsConsistencyGuid -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' .EXAMPLE Clear-ADSyncToolsMsDsConsistencyGuid -Identity 'User1@Contoso.com' .EXAMPLE 'User1@Contoso.com' | Clear-ADSyncToolsMsDsConsistencyGuid #> Function Clear-ADSyncToolsMsDsConsistencyGuid { [CmdletBinding()] Param ( # Target object in AD to clear mS-DS-ConsistencyGuid [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity ) # Get target object from AD $adObject = Search-ADSyncToolsADobject -Identity $Identity Write-Verbose "Found Object '$($adObject.Name)' | mS-DS-ConsistencyGuid: $($adObject.'mS-DS-ConsistencyGuid')" # Get the target Writable DC $domainDNS = Get-ADSyncToolsDomainDns -DistinguishedName $adObject.DistinguishedName $targetDC = Get-ADSyncToolsADtargetDC -Service Writable -DomainName $domainDNS If ($adObject) { Set-ADObject -Identity $adObject.DistinguishedName -Clear 'mS-DS-ConsistencyGuid' -Server $targetDC } } <# .Synopsis Convert Base64 ImmutableId (SourceAnchor) to GUID value .DESCRIPTION Converts value of the ImmutableID from Base64 string and returns a GUID value In case Base64 string cannot be converted to GUID, returns a Byte Array. .EXAMPLE ConvertFrom-ADSyncToolsImmutableID 'iGhmiAEBERG7uxI0VniQqw==' .EXAMPLE 'iGhmiAEBERG7uxI0VniQqw==' | ConvertFrom-ADSyncToolsImmutableID #> Function ConvertFrom-ADSyncToolsImmutableID { [CmdletBinding()] Param ( # ImmutableId in Base64 format [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [string] $Value ) Try { # Try to decode from Base64 $targetValueFromB64 = [system.convert]::FromBase64String($Value) Write-Verbose "Value converted from Base64 string." } Catch { # Could not convert from Base64 Throw "Value '$Value' is not a valid Base64 string." } If ($targetValueFromB64 -ne $null) { Try { # Try to convert to GUID $targetValue = [GUID] $targetValueFromB64 Write-Verbose "Value converted from Base64 string to Guid." } Catch { # Could not convert Base64 to GUID Write-Error "$Value cannot be converted to a GUID value: $($_.Exception.Message)" Write-Warning "Returning result as a byte array:" $targetValue = $targetValueFromB64 } } Return $targetValue } <# .Synopsis Convert GUID (ObjectGUID / ms-Ds-Consistency-Guid) to a Base64 string .DESCRIPTION Converts a value in GUID, GUID string or byte array format to a Base64 string .EXAMPLE ConvertTo-ADSyncToolsImmutableID '88888888-0101-3333-cccc-1234567890cd' .EXAMPLE '88888888-0101-3333-cccc-1234567890cd' | ConvertTo-ADSyncToolsImmutableID #> Function ConvertTo-ADSyncToolsImmutableID { [CmdletBinding()] Param ( # GUID, GUID string or byte array [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] $Value ) # Value ValidateNotNullOrEmpty If ($Value -eq $null -or $Value -eq "") { Throw "ConvertTo-ADSyncToolsImmutableID : Cannot validate argument on parameter 'Value'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again." } # Convert Value to Byte Array Switch ($Value.GetType().Name) { 'Guid' { # Value is a GUID Write-Verbose "Input value is a GUID object. Converting to byte array..." Try { $valueByteArray = $Value.ToByteArray() } Catch { # Failed convertion to byte array Throw "$Value is not recognized as a valid GUID: $($_.Exception.Message)" } } 'String' { # Value is a GUID string Write-Verbose "Input value is a GUID string. Converting to byte array..." Try { $valueByteArray = $([GUID] $Value).ToByteArray() } Catch { # Failed convertion to byte array Throw "$Value is not recognized as a valid GUID: $($_.Exception.Message)" } } 'Byte[]' { # Value is a Byte Array Write-Verbose "Input value is a byte array. Convertion is not required." $valueByteArray = $Value } Default { # Unknown format Throw "$Value is not recognized as a valid GUID." } } Return [system.convert]::ToBase64String($valueByteArray) } <# .Synopsis Export Azure AD Connect Objects to XML files .DESCRIPTION Exports internal ADSync objects from Metaverse and associated connected objects from Connector Spaces .EXAMPLE Export-ADSyncToolsObjects -ObjectId '9D220D58-0700-E911-80C8-000D3A3614C0' -Source Metaverse .EXAMPLE Export-ADSyncToolsObjects -ObjectId '9e220d58-0700-e911-80c8-000d3a3614c0' -Source ConnectorSpace .EXAMPLE Export-ADSyncToolsObjects -DistinguishedName 'CN=User1,OU=ADSync,DC=Contoso,DC=com' -ConnectorName 'Contoso.com' #> Function Export-ADSyncToolsObjects { [CmdletBinding()] Param ( # ObjectId is the unique identifier of the object in the respective connector space or metaverse [Parameter(ParameterSetName='ObjectId', Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] $ObjectId, # Source is the table where the object resides which can be either ConnectorSpace or Metaverse [Parameter(ParameterSetName='ObjectId', Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] [ValidateSet('ConnectorSpace','Metaverse')] $Source, # DistinguishedName is the identifier of the object in the respective connector space [Parameter(ParameterSetName='DistinguishedName', Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] $DistinguishedName, # ConnectorName is the name of the connector space where the object resides [Parameter(ParameterSetName='DistinguishedName', Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] $ConnectorName, # ExportSerialized exports additional XML files [Parameter(Mandatory=$false, Position=2)] [switch] $ExportSerialized ) # Function init IsAADConnectPresent -MinVersion '1.5.20.0' [string] $functionMsg = "Export-ADSyncToolsObjects :" [string] $paramSetName = $PSCmdlet.ParameterSetName [string] $dateStr = '.\' + (Get-Date).toString('yyyyMMdd-HHmmss') + '_' Write-Verbose "$functionMsg ParameterSetName: $paramSetName" if ($Source -eq 'Metaverse') { # Export objects based on metaverse ObjectId Export-ADSyncMVObject -MVObjectId $ObjectId -Prefix $dateStr -ExportSerialized $ExportSerialized } Else { Switch ($paramSetName) { 'ObjectId' { # Find object based on connector space ObjectId $csObject = Get-ADSyncToolsMVObjFromCSID -CSObjectId $ObjectId } 'DistinguishedName' { # Find object based on distinguished name and connector name $csObject = Get-ADSyncToolsMVObjFromCSDN -DistinguishedName $DistinguishedName -ConnectorName $ConnectorName } } If ($csObject.ConnectedMVObjectId -eq '00000000-0000-0000-0000-000000000000') { # Object is a disconnector - Export CS object only Write-Verbose "$functionMsg Object is not connected to the Metaverse (Disconnector)." Export-ADsyncCSObject -CSObjectId $csObject.ObjectId -Prefix $dateStr -ExportSerialized $ExportSerialized } Else { # Export objects based on metaverse ObjectId Export-ADSyncMVObject -MVObjectId $csObject.ConnectedMVObjectId -Prefix $dateStr -ExportSerialized $ExportSerialized } } } <# .Synopsis Import Azure AD Connect Object from XML file .DESCRIPTION Imports an internal ADSync object from XML file that was exported using Export-ADSyncToolsObjects .EXAMPLE Import-ADSyncToolsObjects -Path .\20210224-003104_81275a23-0168-eb11-80de-00155d188c11_MV.xml #> Function Import-ADSyncToolsObjects { [CmdletBinding()] Param ( # Path for the XML file to import [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [string] $Path ) If (Test-Path $Path) { Try { Import-Clixml $Path } Catch { Write-Error "Unable to import file '$Path'. Error Details: $($_.Exception.Message)" } } Else { Write-Error "File not found." } } <# .SYNOPSIS Find object in Metaverse based on Connector Space ObjectId #> Function Get-ADSyncToolsMVObjFromCSID { [CmdletBinding()] Param ( # CSObjectId is the Id of the object in the respective Connector Space [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] $CSObjectId ) # Function init IsAADConnectPresent -MinVersion '1.5.20.0' [string] $functionMsg = "Get-ADSyncToolsMVObjFromCSID :" Write-Verbose "$functionMsg Searching Connector Space object $CSObjectId ..." Try { # Read object from Connector Space $csObj = Get-ADSyncCSObject -Identifier $CSObjectId } Catch { Throw "$functionMsg Unable to find object in Connector Space. Error Details: $($_.Exception.Message)" } # Return result If ($csObj -ne $null) { Write-Verbose "$functionMsg Found Connector Space ObjectId $($csObj.ObjectId) connected to MV ObjectId $($csObj.ConnectedMVObjectId)." Return $csObj } Else { Throw "$functionMsg Unable to find object '$CSObjectId' in Connector Space." } } <# .SYNOPSIS Find object in Metaverse based on Connector Space DN #> Function Get-ADSyncToolsMVObjFromCSDN { [CmdletBinding()] Param ( # DistinguishedName is the identifier of the object in the respective connector space [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] $DistinguishedName, # ConnectorName is the name of the connector space where the object resides [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] $ConnectorName ) # Function init IsAADConnectPresent -MinVersion '1.5.20.0' [string] $functionMsg = "Get-ADSyncToolsMVObjFromCSDN :" Write-Verbose "$functionMsg Searching for '$DistinguishedName' in '$ConnectorName' ..." Try { # Read object from Connector Space $csObj = Get-ADSyncCSObject -DistinguishedName $DistinguishedName -ConnectorName $ConnectorName } Catch { Throw "$functionMsg Unable to find object in Connector Space. Error Details: $($_.Exception.Message)" } # Return result If ($csObj -ne $null) { Write-Verbose "$functionMsg Found Connector Space ObjectId $($csObj.ObjectId) connected to MV ObjectId $($csObj.ConnectedMVObjectId)." Return $csObj } Else { Throw "$functionMsg Unable to find object '$DistinguishedName' in Connector Space $ConnectorName." } } <# .SYNOPSIS Export an object from Connector Space #> Function Export-ADsyncCSObject { [CmdletBinding()] Param ( # CSObjectId is the Id of the object in the respective Connector Space [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] $CSObjectId, # Prefix is a string value which will be used to prefix the filename (Optional) [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=1)] $Prefix, # ExportSerialized exports additional XML files [Parameter(Mandatory=$false, Position=2)] [bool] $ExportSerialized = $false ) # Function init IsAADConnectPresent -MinVersion '1.5.20.0' [string] $functionMsg = "Export-ADsyncCSObject :" Write-Verbose "$functionMsg Exporting Connector Space object $CSObjectId ..." Try { # Read object from Connector Space $csObj = Get-ADSyncCSObject -Identifier $CSObjectId } Catch { Throw "$functionMsg Unable to find object '$CSObjectId' in Connector Space. Error Details: $($_.Exception.Message)" } # If object found If ($csObj -ne $null) { If ($ExportSerialized) { # Export SerializedXml data $Filename = $Prefix + $CSObjectId + "_CS-Serialized.xml" $csObj.SerializedXml | Out-File $Filename } # Export all properties $Filename = $Prefix + $CSObjectId + "_CS.xml" $csObj | Select ObjectId,` ConnectorId,` ConnectorName,` ConnectorType,` PartitionId,` DistinguishedName,` AnchorValue,` ObjectType,` IsConnector,` HasSyncError,` HasExportError,` ConnectedMVObjectId,` Lineage,` Attributes | Export-Clixml $Filename Write-Verbose "$functionMsg Exported Connector Space object to file '$Filename'." } Else { Throw "$functionMsg Unable to find object in Connector Space." } } <# .SYNOPSIS Export object from Metaverse #> Function Export-ADSyncMVObject { [CmdletBinding()] Param ( # MVObjectId is the Id of the object in the Metaverse [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] $MVObjectId, # Prefix is a string value which will be used to prefix the filename (Optional) [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=1)] $Prefix, # ExportSerialized exports additional XML files [Parameter(Mandatory=$false, Position=2)] [bool] $ExportSerialized = $false ) # Function init IsAADConnectPresent -MinVersion '1.5.20.0' [string] $functionMsg = "Export-ADSyncMVObject :" Write-Verbose "$functionMsg Exporting MV object $MVObjectId ..." Try { # Read object from Metaverse $mvObj = Get-ADSyncMVObject -Identifier $MVObjectId } Catch { Throw "$functionMsg Unable to find object in Metaverse. Error Details: $($_.Exception.Message)" } # If object found If ($mvObj -ne $null) { If ($ExportSerialized) { # Export SerializedXml data $Filename = $Prefix + $MVObjectId + "_MV-Serialized.xml" $mvObj.SerializedXml | Out-File $Filename } # Export Lineage data $Filename = $Prefix + $MVObjectId + "_MV.xml" $mvObj | Select ObjectId, Lineage, Attributes | Export-Clixml $Filename Write-Verbose "$functionMsg Exported MV object to file '$Filename'." # Export all Connected objects from the respective Connector Spaces ForEach ($connector in $mvObj.Lineage) { Export-ADsyncCSObject -CsObjectId $connector.ConnectedCsObjectId -Prefix $Prefix -ExportSerialized $ExportSerialized } } Else { Throw "$functionMsg Unable to find object '$MVObjectId' in Metaverse." } } <# .Synopsis Convert AAD Connector DistinguishedName to ImmutableId .DESCRIPTION Takes an AAD Connector DistinguishedName like CN={514635484D4B376E38307176645973555049486139513D3D} and converts to the respective base64 ImmutableID value, e.g. QF5HMK7n80qvdYsUPIHa9Q== .EXAMPLE ConvertFrom-ADSyncToolsAadDistinguishedName 'CN={514635484D4B376E38307176645973555049486139513D3D}' #> Function ConvertFrom-ADSyncToolsAadDistinguishedName { Param ( # Azure AD Connector Space DistinguishedName [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string] $DistinguishedName ) Import-ADSyncToolsAADConnectorBinaries Try { $result = [Microsoft.Online.DirSync.Extension.Utilities.DNEncoding]::SafeRdnToString($DistinguishedName); } Catch { Throw "Unable to convert DistinguishedName to ImmutableId (SourceAnchor). Error Details: $($_.Exception.Message)" } $result } <# .Synopsis Convert ImmutableId to AAD Connector DistinguishedName .DESCRIPTION Takes an ImmutableId (SourceAnchor) like QF5HMK7n80qvdYsUPIHa9Q== and converts to the respective AAD Connector DistinguishedName value, e.g. CN={514635484D4B376E38307176645973555049486139513D3D} .EXAMPLE ConvertTo-ADSyncToolsAadDistinguishedName 'QF5HMK7n80qvdYsUPIHa9Q==' #> Function ConvertTo-ADSyncToolsAadDistinguishedName { Param ( # ImmutableId (SourceAnchor) [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string] $ImmutableId ) Import-ADSyncToolsAADConnectorBinaries Try { $result = [Microsoft.Online.DirSync.Extension.Utilities.DNEncoding]::StringToSafeRdn($ImmutableId); } Catch { Throw "Unable to convert ImmutableId (SourceAnchor) to AAD Connector DistinguishedName. Error Details: $($_.Exception.Message)" } $result } <# .Synopsis Convert Base64 Anchor to CloudAnchor .DESCRIPTION Takes a Base64 Anchor like VAAAAFUAcwBlAHIAXwBjADcAMgA5ADAAMwBlAGQALQA3ADgAMQA2AC0ANAAxAGMAZAAtADkAMAA2ADYALQBlAGEAYwAzADMAZAAxADcAMQBkADcANwAAAA== and converts to the respective CloudAnchor value, e.g. User_abc12345-1234-abcd-9876-ab0123456789 .EXAMPLE ConvertTo-ADSyncToolsCloudAnchor "VAAAAFUAcwBlAHIAXwBjADcAMgA5ADAAMwBlAGQALQA3ADgAMQA2AC0ANAAxAGMAZAAtADkAMAA2ADYALQBlAGEAYwAzADMAZAAxADcAMQBkADcANwAAAA==" .EXAMPLE "VAAAAFUAcwBlAHIAXwBjADcAMgA5ADAAMwBlAGQALQA3ADgAMQA2AC0ANAAxAGMAZAAtADkAMAA2ADYALQBlAGEAYwAzADMAZAAxADcAMQBkADcANwAAAA==" | ConvertTo-ADSyncToolsCloudAnchor #> Function ConvertTo-ADSyncToolsCloudAnchor { Param ( # Base64 Anchor [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string] $Anchor ) $encodedRawAnchor = [System.Convert]::FromBase64String($Anchor); $rawAnchor = $encodedRawAnchor[4..($encodedRawAnchor.Length - 3)] $cloudAnchor = [System.Text.Encoding]::Unicode.GetString($rawAnchor) $cloudAnchor } <# .Synopsis Export Azure AD Disconnector objects .DESCRIPTION Executes CSExport tool to export all Disconnectors to XML and then takes this XML output and converts it to a CSV file with: UserPrincipalName, Mail, SourceAnchor, DistinguishedName, CsObjectId, ObjectType, ConnectorId, CloudAnchor .EXAMPLE Export-ADSyncToolsDisconnectors -SyncObjectType 'PublicFolder' Exports to CSV all PublicFolder Disconnector objects .EXAMPLE Export-ADSyncToolsDisconnectors Exports to CSV all Disconnector objects .INPUTS Use ObjectType argument in case you want to export Disconnectors for a given object type only .OUTPUTS Exports a CSV file with Disconnector objects containing: UserPrincipalName, Mail, SourceAnchor, DistinguishedName, CsObjectId, ObjectType, ConnectorId and CloudAnchor #> Function Export-ADSyncToolsAadDisconnectors { [CmdletBinding()] Param ( # ObjectType to include in output [Parameter(Mandatory=$false, Position=0, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("User", "Group", "Contact", "PublicFolder", "Device")] $SyncObjectType ) IsAADConnectPresent # Get Azure AD Connector name Try { $aadConnectorName = (Get-ADSyncConnector | Where-Object {$_.Identifier -eq 'b891884f-051e-4a83-95af-2544101c9083'}).Name } Catch { Throw "Error: Unable to retrieve Azure AD Connector name. Make sure ADSync service is running. `nDetails: $($_.Exception.Message)" } # Export Disconnectors to XML with CSExport tool $targetFilename = [string] "$(Get-Date -Format yyyyMMdd-HHmmss)_Disconnectors" $cmd = Join-Path -Path $(Get-ADSyncToolsADsyncFolder) -ChildPath 'Bin\csexport.exe' Write-Verbose "Executing command : $cmd" $result = & $cmd $($aadConnectorName) $($targetFilename + '.xml') '/f:s /o:h' If ($lastexitcode -eq 0) { $result } Else { Throw "Error: Unable to retrieve Disconnector objects with CSExport tool. `nDetails: $result" } # Process Disconnector objects from XML output Try { [xml] $disconnectors = Get-Content $($targetFilename + '.xml') } Catch { Throw "Error: Unable to read Disconnector XML file. Error Details: $($_.Exception.Message)" } # Filter out ObjectType If ($SyncObjectType -eq $null) { $disconnectorObjs = $disconnectors.'cs-objects'.'cs-object' Write-Host "Exporting $($disconnectorObjs.Count) Disconnector objects..." } Else { $disconnectorObjs = $disconnectors.'cs-objects'.'cs-object' | Where-Object {$_.'object-type' -eq $SyncObjectType} Write-Host "Exporting $($disconnectorObjs.Count) Disconnector ($SyncObjectType) objects..." } # Export to CSV file $results = @() ForEach ($obj in $disconnectorObjs) { $row = "" | select UserPrincipalName, Mail, SourceAnchor, DistinguishedName, CsObjectId, ObjectType, ConnectorId, CloudAnchor $row.UserPrincipalName = ($obj.'pending-import-hologram'.entry.attr | Where-Object {$_.name -eq 'userPrincipalName'}).Value $row.Mail = ($obj.'pending-import-hologram'.entry.attr | Where-Object {$_.name -eq 'mail'}).Value $row.SourceAnchor = ($obj.'pending-import-hologram'.entry.attr | where {$_.name -eq 'sourceAnchor'}).Value $row.DistinguishedName = $obj.'cs-dn' $row.CsObjectId = $obj.id $row.ObjectType = $obj.'object-type' $row.ConnectorId = $obj.'ma-id' $row.CloudAnchor = ($obj.'pending-import-hologram'.entry.attr | Where-Object {$_.name -eq 'cloudAnchor'}).Value $results += $row } $results | Export-Csv -Path $($targetFilename + '.csv') -NoTypeInformation } <# .Synopsis Get synced objects for a given SyncObjectType .DESCRIPTION Reads from Azure AD all synced objects for a given object class (SyncObjectType). .EXAMPLE Get-ADSyncToolsAadObject -SyncObjectType 'publicFolder' -Credential $(Get-Credential) .OUTPUTS This cmdlet returns the "Shadow" properties that are synchronized by the sync client, which might be different than the actual value stored in the respective property of Azure AD. For instante, a user's UPN that is synchronized with a non-verified domain suffix 'user@nonverified.domain', will have the UPN suffix in Azure AD converted to the tenant's default domain, 'user@tenantname.onmicrosoft.com' In this case, Get-ADSyncToolsAadObject will return the "Shadow" value of 'user@nonverified.domain', and not the actual value in Azure AD 'user@tenantname.onmicrosoft.com' #> Function Get-ADSyncToolsAadObject { [CmdletBinding()] Param ( # Object Type [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("User", "Group", "Contact", "PublicFolder", "Device")] $SyncObjectType, # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential, # Properties from Azure AD to output [Parameter(Mandatory=$false, Position=2)] $Properties ) # BEGIN Import-ADSyncToolsAADConnectorBinaries # Check user's UserPrincipalName format Get-ADSyncToolsUPNsuffix -UserPrincipalName $Credential.UserName | Out-Null Try { $enumerator = [Microsoft.Azure.ActiveDirectory.Connector.Diagnostics.DiagnosticsFactory]::CreateChangeEnumerator(` $Credential.UserName, ` $Credential.Password, ` $SyncObjectType, ` $null, ` 'Full', ` 2) } Catch { Throw "There was a problem initiating Enumerator component. Error Details: $($_.Exception.Message)" } # Define object properties If ($null -ne $Properties) { $propertyNames = @($Properties) } Else { # Default Properties. Also Supports CloudLegacyExchangeDN and CloudMSExchRecipientDisplayType properties $propertyNames = $defaultAADobjProperties # Add UserPrincipalName for User objects If ($SyncObjectType -eq 'User') { $propertyNames += 'UserPrincipalName' } } $baseProperties = @('ObjectClass', 'ObjectId', 'CloudAnchor') $propertyNames = $propertyNames | where {$_ -notin $baseProperties} $results = [System.Collections.ArrayList]@() $counter = 0 Write-Host $("`rReading DirSyncEnabled objects from Azure AD AdminWebServices:") -ForegroundColor Cyan # PROCESS Do { Try { $batch = $enumerator.EnumerateNextBatch() } Catch { Throw "There was a problem with the Enumerator component. Error Details: $($_.Exception.Message)" } # Show progress $counter += $batch.AadBatch.ResultObjects.Count Write-Host -NoNewline $("`r==> $counter ") -ForegroundColor Cyan If ($batch.AadBatch.ResultObjects.Count -gt 0) { ForEach($entry in $batch.AadBatch.ResultObjects) { # Skip deleted objects If ($entry.SyncOperation -ne 'Delete') { $r = "" | select $baseProperties $r.CloudAnchor = $entry.PropertyValues.CloudAnchor $cloudAnchoraSplit = $r.CloudAnchor -split '_' $r.ObjectClass = $cloudAnchoraSplit[0] $r.ObjectId = $cloudAnchoraSplit[1] # Add all properties as a string value or array of strings $entryValues = $entry.PropertyValues ForEach ($propName in $propertyNames) { $entryPropValue = $entryValues[$propName] If ($null -ne $entryPropValue) { # Check if property is multi-valued or single-valued string If ($entryPropValue -is [System.Collections.Generic.IEnumerable[string]]) { $propValue = @() ForEach ($stringValue in $entryPropValue) { $propValue += $stringValue.ToString() } } Else { $propValue = $entryPropValue.ToString() } } Else { # Note: It's not possible to determine if the attribute is multi-valued or single-value from a null value, # hence a null multi-valued attribute will be always returned as an empty single-value string. $propValue = "" } # Add property/value Add-Member -InputObject $r -MemberType NoteProperty -Name $propName -Value $propValue -Force } $null = $results.Add($r) } } } } Until ($batch.AadBatch.MoreToRead -eq $false) Write-Host #END $enumerator.Dispose() $results } <# .Synopsis Exports all synced Mail-Enabled Public Folder objects from AzureAD to a CSV file .DESCRIPTION Reads all synced Mail-Enabled PublicFolder objects from Azure AD and exports the data to a CSV file. .EXAMPLE Export-ADSyncToolsAadPublicFolders -Credential $(Get-Credential) -Path <filename> .OUTPUTS This cmdlet creates the <filename> proficed containing all synced Mail-Enabled PublicFolder objects in CSV format. #> Function Export-ADSyncToolsAadPublicFolders { [CmdletBinding()] Param ( # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential, # Path for output file [Parameter(Mandatory=$true, Position=1)] $Path ) $results = Get-ADSyncToolsAadObject -SyncObjectType PublicFolder -Credential $Credential if ($results.count -gt 0) { Try { $results | Export-Csv -Path $Path -NoTypeInformation -ErrorAction Stop Write-Host "Results exported to file '$Path' successfully." -ForegroundColor Green } Catch { Throw "There was a problem exporting data to file '$Path'. Error Details: $($_.Exception.Message)" } } } <# .Synopsis Removes orphaned Mail-Enabled Public Folder object(s) from Azure AD .DESCRIPTION Deletes synced Public Folder object(s) from Azure AD based on a CSV file or a single SourceAnchor .EXAMPLE Remove-ADSyncToolsAadPublicFolders -InputCsvFilename .\DeleteObjects.csv -Credential (Get-Credential) .EXAMPLE Remove-ADSyncToolsAadPublicFolders -SourceAnchor '2epFRNMCPUqhysJL3SWL1A==' -Credential (Get-Credential) .INPUTS The CSV input file can be generated using Export-ADSyncToolsAadPublicFolders Path parameters must point to a CSV file with at least 2 columns: SourceAnchor, SyncObjectType .OUTPUTS Shows results from ExportDeletions operation .NOTES DISCLAIMER: Synced Mail-Enabled Public Folder objects deleted with this function cannot be RECOVERED! #> Function Remove-ADSyncToolsAadPublicFolders { [CmdletBinding(SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='High')] Param ( # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential, # CSV Input filename - Use Export-ADSyncToolsAadPublicFolders [Parameter(ParameterSetName='CsvInput', Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $InputCsvFilename, # Object SourceAnchor [Parameter(ParameterSetName='ObjectInput', Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $SourceAnchor ) Write-Verbose "ParameterSetName: $($PSCmdlet.ParameterSetName)" switch ($PSCmdlet.ParameterSetName) { 'CsvInput' { # SoA seizure Set-ADSyncToolsAadObject -Credential $Credential -SyncOperation Add -InputCsvFilename $InputCsvFilename # Delete MEPF Set-ADSyncToolsAadObject -Credential $Credential -SyncOperation Delete -InputCsvFilename $InputCsvFilename } 'ObjectInput' { # SoA seizure Set-ADSyncToolsAadObject -Credential $Credential -SyncOperation Add -SourceAnchor $SourceAnchor -SyncObjectType "PublicFolder" # Delete MEPF Set-ADSyncToolsAadObject -Credential $Credential -SyncOperation Delete -SourceAnchor $SourceAnchor -SyncObjectType "PublicFolder" } Default { Throw "Invalid Parameter set. Please try again." } } } <# .Synopsis Remove orphaned synced object from Azure AD .DESCRIPTION Deletes from Azure AD a synced object(s) based on SourceAnchor and ObjecType in batches of 10 objects The CSV file can be generated using Export-ADSyncToolsAadDisconnectors .EXAMPLE Remove-ADSyncToolsAadObject -InputCsvFilename .\DeleteObjects.csv -Credential (Get-Credential) .EXAMPLE Remove-ADSyncToolsAadObject -SourceAnchor '2epFRNMCPUqhysJL3SWL1A==' -SyncObjectType 'publicFolder' -Credential (Get-Credential) .INPUTS InputCsvFilename must point to a CSV file with at least 2 columns: SourceAnchor, SyncObjectType .OUTPUTS Shows results from ExportDeletions operation .NOTES DISCLAIMER: Other than User objects that have a Recycle Bin, any other object types DELETED with this function cannot be RECOVERED! #> Function Remove-ADSyncToolsAadObject { [CmdletBinding(SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='High')] Param ( # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential, # CSV Input filename - Must contain header: ObjectClass,SourceAnchor [Parameter(ParameterSetName='CsvInput', Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $InputCsvFilename, # Object SourceAnchor [Parameter(ParameterSetName='ObjectInput', Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $SourceAnchor, # Object Type [Parameter(ParameterSetName='ObjectInput', Mandatory=$true, Position=2, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("User", "Group", "Contact", "PublicFolder")] $SyncObjectType ) Write-Verbose "ParameterSetName: $($PSCmdlet.ParameterSetName)" switch ($PSCmdlet.ParameterSetName) { 'CsvInput' { Set-ADSyncToolsAadObject -Credential $Credential -SyncOperation Delete -InputCsvFilename $InputCsvFilename } 'ObjectInput' { Set-ADSyncToolsAadObject -Credential $Credential -SyncOperation Delete -SourceAnchor $SourceAnchor -SyncObjectType $SyncObjectType } Default { Throw "Invalid Parameter set. Please try again." } } } <# .SYNOPSIS Adds or Deletes object(s) in Azure AD #> Function Set-ADSyncToolsAadObject { [CmdletBinding()] Param ( # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential, # Sync Operation [Parameter(Mandatory=$true, Position=1)] [ValidateNotNullOrEmpty()] [ValidateSet("Add", "Delete")] $SyncOperation, # CSV Input filename - Must contain header: ObjectClass,SourceAnchor [Parameter(ParameterSetName='CsvInput', Mandatory=$true, Position=2, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $InputCsvFilename, # Object SourceAnchor [Parameter(ParameterSetName='ObjectInput', Mandatory=$true, Position=2, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $SourceAnchor, # Object Type [Parameter(ParameterSetName='ObjectInput', Mandatory=$true, Position=3, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [ValidateSet("User", "Group", "Contact", "PublicFolder")] $SyncObjectType ) # BEGIN Write-Verbose "ParameterSetName: $($PSCmdlet.ParameterSetName)" # Check user's UserPrincipalName format Get-ADSyncToolsUPNsuffix -UserPrincipalName $Credential.UserName | Out-Null If ($($PSCmdlet.ParameterSetName) -eq 'CsvInput') { # Import CSV file Try { $objects = @(Import-Csv $InputCsvFilename) Write-Verbose "CsvInput: $InputCsvFilename | ObjectCount = $($objects.Count)" } Catch { Throw "There was a problem importing and processing CSV input file. Error Details: $($_.Exception.Message)" } # Set objectClass camel-case Try { for ($i = 0; $i -lt $objects.count; $i++) { [string] $syncObjectType = $objects[$i].ObjectClass $objects[$i].ObjectClass = $syncObjectType[0].ToString().ToLower() + $syncObjectType.Substring(1) } } Catch { Throw "There was a problem preparing 'ObjectClass' property. Error Details: $($_.Exception.Message)" } } Else { $object = "" | Select ObjectClass,SourceAnchor $object.ObjectClass = $SyncObjectType[0].ToString().ToLower() + $SyncObjectType.Substring(1) $object.SourceAnchor = $SourceAnchor Write-Verbose "ObjectInput: $($object.ObjectClass) | $($object.SourceAnchor)" $objects = @($object) } Switch ($SyncOperation) { 'Add' { Set-ADSyncToolsAadObjectAdd -Credential $Credential -Objects $objects } 'Delete' { Set-ADSyncToolsAadObjectDelete -Credential $Credential -Objects $objects } Default { Throw "Invalid 'SyncOperation' input. Please try again." } } } <# .SYNOPSIS Adds object(s) to Azure AD #> Function Set-ADSyncToolsAadObjectAdd { [CmdletBinding(SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='High')] Param ( # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential, # Sync Operation [Parameter(Mandatory=$true, Position=1)] [ValidateNotNullOrEmpty()] $Objects ) # BEGIN Import-ADSyncToolsAADConnectorBinaries Try { $exporter = ` [Microsoft.Azure.ActiveDirectory.Connector.Diagnostics.DiagnosticsFactory]::CreateDirectoryChangeExporter( ` $Credential.UserName, ` $Credential.Password) } Catch { Throw "There was a problem initiating Exporter component. Error Details: $($_.Exception.Message)" } Try { $entries = [System.Collections.ArrayList]@() ForEach ($obj in $objects) { Write-Verbose "Processing: SyncEntry = $($obj.ObjectClass) | $($obj.SourceAnchor)" # Add SyncEntry $syncObject = New-Object Microsoft.Online.Coexistence.Schema.AzureADSyncObject $syncObject.SyncObjectType = [Microsoft.Online.Coexistence.Schema.SyncObjectType]::PublicFolder $syncObject.SyncOperation = [Microsoft.Online.Coexistence.Schema.SyncObjectOperation]::Add $syncObject.SetValue([Microsoft.Online.Coexistence.Schema.SyncObjectAttributes]::SourceAnchor, $obj.SourceAnchor) $entries.Add($syncObject) | Out-Null } } Catch { Throw "There was a problem creating Sync entries. Error Details: $($_.Exception.Message)" } $objsCount = $objects.Count $objsProcessed = 0 $batchSize = 10 $nextBatch = @() # PROCESS Try { While ($objsProcessed -lt $objsCount) { # Progress bar $percent = [math]::Round($(($objsProcessed * 100) / $objsCount), 1) Write-Progress -Activity "Exporting objects to Azure AD" -Status "$($percent)% Complete:" -PercentComplete $percent; # Process batch $nextBatch = $entries | Select-Object -First $batchSize $entries = $entries | Select-Object -Skip $batchSize #$nextBatch | Out-String # Export objects $results = $exporter.Export([Microsoft.Online.Coexistence.Schema.AzureADSyncObject[]]$nextBatch, 2) Write-Output $results | FT ResultCode, ResultErrorDescription, ObjectType, SourceAnchor $objsProcessed += $nextBatch.Count } } Catch { Throw "There was a problem processing the batch. Error Details: $($_.Exception.Message)" } #END $exporter.Dispose() } <# .SYNOPSIS Deletes object(s) from Azure AD #> Function Set-ADSyncToolsAadObjectDelete { [CmdletBinding(SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='High')] Param ( # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential, # Sync Operation [Parameter(Mandatory=$true, Position=1)] [ValidateNotNullOrEmpty()] $Objects ) # BEGIN Import-ADSyncToolsAADConnectorBinaries Try { $exporter = ` [Microsoft.Azure.ActiveDirectory.Connector.Diagnostics.DiagnosticsFactory]::CreateDirectoryChangeExporter( ` $Credential.UserName, ` $Credential.Password) } Catch { Throw "There was a problem initiating Exporter component. Error Details: $($_.Exception.Message)" } Try { $entries = [System.Collections.ArrayList]@() ForEach ($obj in $objects) { Write-Verbose "Processing: DeleteEntry = $($obj.ObjectClass) | $($obj.SourceAnchor)" # Add DeleteEntry $entries.Add([Microsoft.Azure.ActiveDirectory.Connector.Diagnostics.DeletionEntry]::FromSourceAnchor($obj.ObjectClass, $obj.SourceAnchor)) | Out-Null } } Catch { Throw "There was a problem creating Deletion entries. Error Details: $($_.Exception.Message)" } $objsCount = $objects.Count $objsProcessed = 0 $batchSize = 10 $nextBatch = @() # PROCESS Try { While ($objsProcessed -lt $objsCount) { # Progress bar $percent = [math]::Round($(($objsProcessed * 100) / $objsCount), 1) Write-Progress -Activity "Deleting objects from Azure AD" -Status "$($percent)% Complete:" -PercentComplete $percent; # Process batch $nextBatch = $entries | Select-Object -First $batchSize $entries = $entries | Select-Object -Skip $batchSize #$nextBatch | Out-String if ($pscmdlet.ShouldProcess("$($nextBatch.Count) objects", "Delete from Azure AD")) { $results = $exporter.ExportDeletions([Microsoft.Azure.ActiveDirectory.Connector.Diagnostics.DeletionEntry[]]$nextBatch) Write-Output $results | FT ResultCode, ResultErrorDescription, ObjectType, SourceAnchor } else { Write-Output "SKIPPED: Operation canceled. `n`n" } $objsProcessed += $nextBatch.Count } } Catch { Throw "There was a problem processing the batch. Error Details: $($_.Exception.Message)" } #END $exporter.Dispose() } <# .Synopsis Get Azure AD Connnect Run History .DESCRIPTION Function that returns the Azure AD Connect Run History in XML format .EXAMPLE Get-ADSyncToolsRunHistory .EXAMPLE Get-ADSyncToolsRunHistory -Days 3 #> Function Get-ADSyncToolsRunHistory { Param ( # Number of days back to collect History (default = 1) [Parameter(Mandatory=$false)] [int] $Days = 1 ) IsAADConnectPresent -MinVersion '1.4.18.0' # Read Run Profile Try { If ($Days -eq 0) { $runProfile = Get-ADSyncRunProfileResult -ErrorAction Stop } Else { $startDate = (Get-Date).AddDays(-$Days).ToUniversalTime() $runProfile = Get-ADSyncRunProfileResult -ErrorAction Stop | where {$_.StartDate -gt $startDate} } } Catch { Throw "There was a problem calling 'Get-ADSyncRunProfileResult': $($_.Exception.Message)" } $runProfile | select ConnectorName, RunProfileName, Result, StartDate, EndDate, CurrentStepNumber, RunStepResults, RunHistoryId } <# .Synopsis Shows the Run Profile history merged with Run Step results .DESCRIPTION Gets ADSync Run Profile history including each Run Step result .EXAMPLE Get-ADSyncToolsRunStepHistory | FT .EXAMPLE Get-ADSyncToolsRunStepHistory -FromStartDate "9/25/2021 7:38" | FT #> Function Get-ADSyncToolsRunStepHistory { [CmdletBinding()] Param( # Filter from start date [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=0)] [datetime] $FromStartDate ) IsAADConnectPresent -MinVersion '1.4.18.0' # BEGIN $profileProperties = @('StartDate','EndDate','ConnectorName','RunProfileName','RunNumber','RunHistoryId') $stepProperties = @('StepNumber','StepResult','StepHistoryId') $allProperties = @('StartDate','EndDate','ConnectorName','RunProfileName','StepResult','StepNumber','RunNumber','RunHistoryId','StepHistoryId') $results = @() If ($FromStartDate -ne $null) { $runHistory = Get-ADSyncRunProfileResult | Where StartDate -gt $FromStartDate | select $profileProperties } Else { $runHistory = Get-ADSyncRunProfileResult | select $profileProperties } # PROCESS ForEach ($runProfile in $runHistory) { $r = "" | select $allProperties ForEach ($p in $profileProperties) { $r.$p = $runProfile.$p } ForEach ($runStep in (Get-ADSyncRunStepResult -RunHistoryId $r.RunHistoryId | select $stepProperties)) { ForEach ($p in $stepProperties) { $r.$p = $runStep.$p } $results += ($r | select *) } } # END $results } <# .Synopsis Export Azure AD Connnect Run History .DESCRIPTION Function to export Azure AD Connect Run Profile and Run Step results to CSV and XML format respectively. The resulting Run Profile CSV file can be imported into a spreadsheet and the Run Step XML file can be imported with Import-Clixml .EXAMPLE Export-ADSyncToolsRunHistory -TargetName MyADSyncHistory #> Function Export-ADSyncToolsRunHistory { Param ( # Name of the output file [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [String] $TargetName ) IsAADConnectPresent -MinVersion '1.4.18.0' # Read Run Profile and Run Step results Try { $runProfile = Get-ADSyncRunProfileResult -ErrorAction Stop $runSteps = Get-ADSyncRunStepResult -ErrorAction Stop } Catch { Throw "There was a problem calling 'Get-ADSyncRunProfileResult' and 'Get-ADSyncRunStepResult': $($_.Exception.Message)" } # Export Run Profile results Try { $runProfile | Where-Object {$_.IsRunComplete -eq 'True'} | select ConnectorName, RunProfileName, Result, StartDate, EndDate, CurrentStepNumber, RunStepResults, RunHistoryId | Export-Csv ".\$TargetName-RunProfile.csv" -NoTypeInformation -ErrorAction Stop } Catch { Throw "There was a problem exporting Run Profile results: $($_.Exception.Message)" } # Export Run Step results Try { $runSteps | Export-Clixml ".\$TargetName-RunStep.xml" -ErrorAction Stop } Catch { Throw "There was a problem exporting Run Step results: $($_.Exception.Message)" } } <# .Synopsis Import Azure AD Connnect Run History .DESCRIPTION Function to Import Azure AD Connect Run Step results from XML created using Export-ADSyncToolsRunHistory .EXAMPLE Export-ADSyncToolsRunHistory -Path .\RunHistory-RunStep.xml #> Function Import-ADSyncToolsRunHistory { [CmdletBinding()] Param ( # Path for the XML file to import [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [string] $Path ) If (Test-Path $Path) { Try { Import-Clixml $Path } Catch { Write-Error "Unable to import file '$Path'. Error Details: $($_.Exception.Message)" } } Else { Write-Error "File not found." } } <# .Synopsis Get Azure AD Connect Run History for older versions of AADConnect (WMI) .DESCRIPTION Function that returns the Azure AD Connect Run History in XML format .EXAMPLE Get-ADSyncToolsRunHistory .EXAMPLE Get-ADSyncToolsRunHistory -Days 3 #> Function Get-ADSyncToolsRunHistoryLegacyWmi { Param ( # Number of days back to collect History (default = 1) [Parameter(Mandatory=$false)] [int] $Days = 1 ) IsAADConnectPresent -MaxVersion '1.4.0.0' $runStartDate=(Get-Date (Get-Date).AddDays(-$Days) -Format yyyy-MM-dd) $getRunStartTime="RunStartTime >'"+$runStartDate+"'" $namespace = 'root\MicrosoftIdentityintegrationServer' Try { $miis_RunHistory = Get-WmiObject -class "MIIS_RunHistory" -namespace $namespace -Filter $getRunStartTime -ErrorAction Stop } Catch { $errorMsg = "There was a problem calling WMI Namespace '$namespace': $($_.Exception.Message)" Write-Error $errorMsg return @($errorMsg) } If ($miis_RunHistory -ne $null) { $xmlData = @() ForEach ($entry in $miis_RunHistory) { $xmlData += $entry.RunDetails() } Return $xmlData } } <# .Synopsis Script to Remove Expired Certificates from UserCertificate Attribute .DESCRIPTION This script takes all the objects from a target Organizational Unit in your Active Directory domain - filtered by Object Class (User/Computer) and deletes all expired certificates present in the UserCertificate attribute. By default (BackupOnly mode) it will only backup expired certificates to a file and not do any changes in AD. If you use -BackupOnly $false then any Expired Certificate present in UserCertificate attribute for these objects will be removed from Active Directory after being copied to file. Each certificate will be backed up to a separated filename: ObjectClass_ObjectGUID_CertThumprint.cer The script will also create a log file in CSV format showing all the users with certificates that either are valid or expired including the actual action taken (Skipped/Exported/Deleted). .EXAMPLE Check all users in target OU - Expired Certificates will be copied to separated files and no certificates will be removed Remove-ADSyncToolsExpiredCertificates -TargetOU "OU=Users,OU=Corp,DC=Contoso,DC=com" -ObjectClass user .EXAMPLE Delete Expired Certs from all Computer objects in target OU - Expired Certificates will be copied to files and removed from AD Remove-ADSyncToolsExpiredCertificates -TargetOU "OU=Computers,OU=Corp,DC=Contoso,DC=com" -ObjectClass computer -BackupOnly $false #> Function Remove-ADSyncToolsExpiredCertificates { [CmdletBinding()] Param ( # Target OU to lookup for AD objects [Parameter(Mandatory=$True)] [string]$TargetOU, # BackupOnly will not delete any certificates from AD [Bool]$BackupOnly = $True, # Object Class filter [Parameter(Mandatory=$True)] [ValidateSet('user','computer')] [String] $ObjectClass ) Import-ADSyncToolsActiveDirectoryModule # Query AD object class = $ObjectClass that contain UserCerts in OU = $TargetOU $ldapFilter = [string] "(objectClass=$ObjectClass)" $adObjectsInOU = @(Get-ADObject -LDAPFilter $ldapFilter -SearchBase $TargetOU -Properties userCertificate | where {$_.userCertificate -ne $null}) Write-Output "Processing $($adObjectsInOU.Count) AD objects with UserCertificate..." # Backup removed certificates to a file [bool] $BackupCertificates = $True # For each user and each cert check validity, backup to a file (if $BackupCertificates = $True) and remove cert it if Expired $resultsTable = @() $today = Get-Date foreach ($adObject in $adObjectsInOU) { $objCerts = @($adObject.UserCertificate) Write-Output "Checking AD Object: $($adObject.Name) | Total Certs: $($objCerts.Count)" $certIndex = 0 foreach ($cert in $objCerts) { $row = "" | select ADobjectDN,CertificateIndex,CertificateName,CertificateTemplate,CertificateIssuingDate,CertificateExpireDate,CertificateStatus,Export-Action-Reason $certObj = [System.Security.Cryptography.X509Certificates.X509Certificate2] $cert $certName = $certObj.GetName() $certTemplate = $certObj.Extensions| foreach { $_.Format($false) | Select-String "Template=" } Write-Debug $certObj $templateName = @($($certTemplate -split ','))[0] if ($templateName -eq $null) { $templateName = "N/A" } if ($certObj.NotAfter -lt $($today)) { $certStatus = "Expired" $deleteCert = $true if ($BackupCertificates) { $filename = [string] ".\$($ObjectClass)_$($adObject.ObjectGUID)_$($certObj.Thumbprint).cer" Try { $exportResult = Export-Certificate -Cert $certObj -FilePath $filename $row.'Export-Action-Reason' = "Exported" Write-Output "Expired Certificate exported to file: $($exportResult.Name)" } Catch { $row.'Export-Action-Reason' = "ExportFailed-NotRemoved-Error" $deleteCert = $false } } } else { $certStatus = "Valid" $deleteCert = $false $row.'Export-Action-Reason' += "Skipped-NotRemoved-ValidCert)" } if ($deleteCert) { Try { if (!$BackupOnly) { Set-ADObject -Identity $adObject.DistinguishedName -Remove @{UserCertificate=$certObj} $row.'Export-Action-Reason' += "-Removed-Expired" } else { $row.'Export-Action-Reason' += "-ToBeRemoved-BackupOnly" } } Catch { $row.'Export-Action-Reason' += "-NotRemoved-Error" } } $row.ADobjectDN = [string] $adObject.DistinguishedName.ToString() $row.CertificateIndex = [string] $certIndex.ToString() $row.CertificateName = [string] $certObj.GetName() $row.CertificateTemplate = [string] $templateName.ToString() $row.CertificateIssuingDate = [string] $certObj.NotBefore.ToString() $row.CertificateExpireDate = [string] $certObj.NotAfter.ToString() $row.CertificateStatus = [string] $certStatus.ToString() Write-Verbose $row $resultsTable += $row | Select * $certIndex++ } } # Export results to a file $date = [string] $(Get-Date -Format yyyyMMddHHmmss) $filename = [string] ".\ExpiredCertsResults-$date.txt" $resultsTable | Export-Csv -Path $filename -Delimiter "`t" -NoTypeInformation } <# .Synopsis Creates a trace file from an Active Directory Import Step .DESCRIPTION Traces all LDAP queries of an Active Directory Import run from a given Active Directory watermark checkpoint (aka. partition cookie). Creates a trace file '.\ADimportTrace_yyyyMMddHHmmss.log' on the current folder. To use -ADConnectorXML, go to the Synchronization Service Manager, right-click your AD Connector and select "Export Connector..." .EXAMPLE Trace Active Directory Import for user objects by providing an AD Connector XML file Trace-ADSyncToolsADImport -DC 'DC1.contoso.com' -RootDN 'DC=Contoso,DC=com' -Filter '(&(objectClass=user))' -ADConnectorXML .\ADConnector.xml .EXAMPLE Trace Active Directory Import for all objects by providing the Active Directory watermark (cookie) and AD Connector credential $creds = Get-Credential Trace-ADSyncToolsADImport -DC 'DC1.contoso.com' -RootDN 'DC=Contoso,DC=com' -Credential $creds -ADwatermark "TVNEUwMAAAAXyK9ir1zSAQAAAAAAAAAA(...)" #> Function Trace-ADSyncToolsADImport { [CmdletBinding()] Param ( # Target Domain Controller [Parameter( Mandatory=$True, Position=0)] [string] $DC, # Forest Root DN [Parameter( Mandatory=$True, Position=1)] [string] $RootDN, # AD objects type to trace. Use '(&(objectClass=*))' for all object types [Parameter( Mandatory=$False, Position=2)] [string] $Filter = '(&(objectClass=*))', # Provide the credential to run LDAP query against AD [Parameter( Mandatory=$false, Position=3)] [PSCredential] $Credential, # SSL Connection [Parameter( Mandatory=$false, Position=4)] [switch] $SSL = $false, # AD Connector Export XML file - Right-click AD Connector and select "Export Connector..." [Parameter( Mandatory=$True, Position=5, ParameterSetName = "ADConnectorXML")] [string] $ADConnectorXML, # Manual input of watermark, instead of XML file e.g. $ADwatermark = "TVNEUwMAAAAXyK9ir1zSAQAAAAAAAAAA(...)" [Parameter( Mandatory=$True, Position=5, ParameterSetName = "ADwatermarkInput")] [string] $ADwatermark ) # Read AD watermark value If ($ADwatermark -eq "" -or $ADwatermark -eq $null) { # Read AD Connector XMl file If ($ADConnectorXML -notlike "" -and (Test-Path $ADConnectorXML)) { # Parse the Cookie (AD watermark) from the XML data Try { $adcsXMLdata = [xml] (Get-Content $ADConnectorXML) $maPartitionDataList = @($adcsXMLdata.'saved-ma-configuration'.'ma-data'.'ma-partition-data'.partition) $maPartitionData = $maPartitionDataList | Where-Object {$_.Name -like $RootDN} $ADwatermark = $maPartitionData.'custom-data'.'adma-partition-data'.cookie Write-Verbose "AD watermark from AD Connector XML file '$ADConnectorXML': `n$ADwatermark `n" } Catch { Throw "Error reading AD Connector XML export file: $($_.Exception.Message)" } } Else { Throw "Please provide a valid AD Connector XML export file." } } # Parse AD watermark value Write-Host "`Parsing AD watermark for '$RootDN' partition: `n$ADwatermark `n" Try { [byte[]] $dirSyncCookie = [System.Convert]::FromBase64String($ADwatermark) } Catch { Throw "Error parsing AD watermark: $($_.Exception.Message)" } # Importing from AD Write-Host "`nImporting from AD ..." Try { [void] ([System.Reflection.Assembly]::LoadWithPartialName('System.DirectoryServices.Protocols')) } Catch { Throw "Error loading assemblies: $($_.Exception.Message)" } If (($SSL) -and ($DC -notlike '*:636')) { $DC = '{0}:636' -f $DC } If ($Credential -eq $null) { Write-Verbose "Credential not passed in - Using security context of the current logged-on user." # Use Current Logged on User credential [DirectoryServices.Protocols.LdapConnection] $ldapConn = New-Object DirectoryServices.Protocols.LdapConnection($DC) } Else { # Use provided Credential Write-Verbose "Credential passed in - Using provided credential" Try { [DirectoryServices.Protocols.LdapConnection] $ldapConn = New-Object DirectoryServices.Protocols.LdapConnection($DC, $Credential.GetNetworkCredential()) } Catch { Throw "LDAP connection failure: $($_.Exception.Message)" } } # Generate AD Import trace file $d = "`t" # Delimiter $logfilename = [string] ".\ADimportTrace_$(Get-Date -Format yyyyMMddHHmmss).log" $header = [string] "Timestamp" + $d + "ldapResult" + $d + "ldapCount" + $d + "AttributeCount" + $d + "DistinguishedName" + $d + "Attributes(ValuesCount)" Out-File -FilePath $logfilename -InputObject $header # Setup LDAP request If (-not $SSL) { $ldapConn.SessionOptions.Sealing = $true } Else { $ldapConn.SessionOptions.SecureSocketLayer = $true } [string[]] $attributesToFetch = $null $ldapConn.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos [DirectoryServices.Protocols.SearchRequest] $ldapRequest = New-Object DirectoryServices.Protocols.SearchRequest($RootDN, $Filter, 'SubTree', $attributesToFetch) [DirectoryServices.Protocols.DirSyncRequestControl] $dirSyncCtr = New-Object DirectoryServices.Protocols.DirSyncRequestControl($dirSyncCookie, [DirectoryServices.Protocols.DirectorySynchronizationOptions]::None, [Int32]::MaxValue) [void] $ldapRequest.Controls.Add($dirSyncCtr) [bool] $hasMore = $false # Process LDAP Request/Response Do { [DirectoryServices.Protocols.SearchResponse] $ldapResponse = $null $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Try { $ldapResponse = $ldapConn.SendRequest($ldapRequest) } Catch { Throw "Problem sending LDAP request. Try without SSL or to provide the sync cookie with -ADwatermark <based64> or -ADConnectorXML <file>. Error Details: $($_.Exception.Message)" } # Show/ Log LDAP Response Write-Host ('Search response code: {0}, Result Count {1}' -f $ldapResponse.ResultCode, $ldapResponse.Entries.Count) $logldapResponse = [string] $timestamp + $d + $($ldapResponse.ResultCode) + $d + $($ldapResponse.Entries.Count) ForEach ($entry in $ldapResponse.Entries) { # Show/ Log Entry from LDAP Response Write-Host ('Entry: {0} | Attribute Count = {1}' -f $entry.DistinguishedName, $entry.Attributes.Count) $logdata = [string] $logldapResponse + $d + $($entry.Attributes.Count) + $d + $($entry.DistinguishedName) $attributeData = "" foreach ($attributeName in $entry.Attributes.AttributeNames) { $attributeData += $attributeName + "(" + $($entry.Attributes[$attributeName].Count) + ")" + "," Write-Host ("Attribute {0}, ValueCount {1}" -f $attributeName, $entry.Attributes[$attributeName].Count) } Write-Host $logdata += $d + $attributeData.Substring(0, $attributeData.Length -1) Out-File -FilePath $logfilename -InputObject $logdata -Append } $hasMore = $false If (-not ([object]::Equals($ldapResponse, $null))) { ForEach ($oneLdapResponseControl in $ldapResponse.Controls) { If ($oneLdapResponseControl -is [DirectoryServices.Protocols.DirSyncResponseControl]) { [DirectoryServices.Protocols.DirSyncResponseControl] $dirSyncCtrResponse = [DirectoryServices.Protocols.DirSyncResponseControl] $oneLdapResponseControl $dirSyncCtr.Cookie = $dirSyncCtrResponse.Cookie $hasMore = $dirSyncCtrResponse.MoreData Break } } } } While ($hasMore) } <# .Synopsis Trace LDAP queries .DESCRIPTION Helper function for troubleshooting Active Directory LDAP queries .EXAMPLE Trace-ADSyncToolsLdapSchemaQuery -RootDN "DC=Contoso,DC=com" -Credential $Credential #> Function Trace-ADSyncToolsLdapSchemaQuery { [CmdletBinding()] Param ( # Forest/Domain DistinguishedName [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [String] $RootDN, # AD Credential [Parameter(Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [pscredential] $Credential, # Domain Controller Name (optional) [Parameter(Mandatory=$false, Position=2, ValueFromPipelineByPropertyName=$true)] [String] $Server, # Domain Controller port (default: 389) [Parameter(Mandatory=$false, Position=2, ValueFromPipelineByPropertyName=$true)] [Int] $Port = 389, # LDAP filter (default: objectClass=*) [Parameter(Mandatory=$false)] [String] $Filter = "(objectClass=*)" ) [Reflection.Assembly]::LoadWithPartialName("System.Directoryservices.Protocols") [Reflection.Assembly]::LoadWithPartialName("System.Directoryservices") $ldapDirectoryId = New-Object System.DirectoryServices.Protocols.LdapDirectoryIdentifier($Server, $Port) $ldapConnection = New-Object System.DirectoryServices.Protocols.LdapConnection($ldapDirectoryId, $Credential.GetNetworkCredential()) $rootDSEReq = New-Object System.DirectoryServices.Protocols.SearchRequest $rootDSEReq.DistinguishedName = $RootDN $rootDSEReq.Filter = $Filter $rootDSEReq.Scope = [System.DirectoryServices.Protocols.SearchScope]("Base") $rootDSEReq.SizeLimit = 1 $rootDSEReq.TimeLimit = [System.Timespan]::FromMinutes(2) $rootDSEReq.Attributes.Add("SubschemaSubentry") | Out-Null Try { $searchResponse = [System.DirectoryServices.Protocols.SearchResponse]$ldapConnection.SendRequest($rootDSEReq) } Catch { Throw "There was an error searching Active Directory. Error Details: $($_.Exception.Message). `nInnerException: $($_.Exception.InnerException)" } $subentryDN = $searchResponse.Entries[0].Attributes["SubschemaSubentry"][0] Write-Host "Sub entry DN '$subentryDN' from '$($($searchResponse.Entries[0]).DistinguishedName)'" -ForegroundColor Cyan $seReq = New-Object System.DirectoryServices.Protocols.SearchRequest $seReq.DistinguishedName = $subentryDN $seReq.Filter = $Filter $seReq.Scope = [System.DirectoryServices.Protocols.SearchScope]("Base") $seReq.SizeLimit = 1 $seReq.TimeLimit = [System.Timespan]::FromMinutes(2) $seReq.Attributes.Add("extendedAttributeInfo") | Out-Null $seReq.Attributes.Add("attributeTypes") | Out-Null $seReq.Attributes.Add("objectClasses") | Out-Null $seReq.Attributes.Add("dITContentRules") | Out-Null $searchResponse = [System.DirectoryServices.Protocols.SearchResponse]$ldapConnection.SendRequest($seReq) $logfilenamePrefix = [string] ".\LdapTrace_$(Get-Date -Format yyyyMMddHHmmss)" Write-Host "Exporting data to '$logfilenamePrefix*' files..." -ForegroundColor Cyan $logfilename = $logfilenamePrefix + "-extendedAttributeInfo.txt" for ($i = 0; $i -lt $searchResponse.Entries[0].Attributes["extendedAttributeInfo"].Count; $i++) { Add-Content -Path $logfilename -Value $searchResponse.Entries[0].Attributes["extendedAttributeInfo"][$i] } $logfilename = $logfilenamePrefix + "-attributeTypes.txt" for ($i = 0; $i -lt $searchResponse.Entries[0].Attributes["attributeTypes"].Count; $i++) { Add-Content -Path $logfilename -Value $searchResponse.Entries[0].Attributes["attributeTypes"][$i] } $logfilename = $logfilenamePrefix + "-objectClasses.txt" for ($i = 0; $i -lt $searchResponse.Entries[0].Attributes["objectClasses"].Count; $i++) { Add-Content -Path $logfilename -Value $searchResponse.Entries[0].Attributes["objectClasses"][$i] } $logfilename = $logfilenamePrefix + "-dITContentRules.txt" for ($i = 0; $i -lt $searchResponse.Entries[0].Attributes["dITContentRules"].Count; $i++) { Add-Content -Path $logfilename -Value $searchResponse.Entries[0].Attributes["dITContentRules"][$i] } } <# .Synopsis Trace LDAP queries .DESCRIPTION Helper function for troubleshooting Active Directory LDAP queries .EXAMPLE Trace-ADSyncToolsLdapQuery -RootDN "DC=Contoso,DC=com" -Credential $Credential #> Function Trace-ADSyncToolsLdapQuery { [CmdletBinding()] Param ( # DistinguishedName of base object [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [String] $BaseDN, # AD Credential [Parameter(Mandatory=$false, Position=1, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [pscredential] $Credential, # Domain Controller Name (optional) [Parameter(Mandatory=$false, Position=2, ValueFromPipelineByPropertyName=$true)] [String] $Server, # Domain Controller port (default: 389) [Parameter(Mandatory=$false, Position=2, ValueFromPipelineByPropertyName=$true)] [Int] $Port = 389, # LDAP filter (default: objectClass=domainDNS) [Parameter(Mandatory=$false)] [String] $Filter = "(objectClass=container)", # LDAP SearchScope (default: SubTree) [Parameter(Mandatory=$false)] [String] $SearchScope = "SubTree", # LDAP SizeLimit (default: 0) [Parameter(Mandatory=$false)] [Int] $SizeLimit = 0, # LDAP TimeLimit in seconds (default: 120) [Parameter(Mandatory=$false)] [Int] $TimeLimitSeconds = 120 ) # Load System.Directoryservices Write-Verbose "Init: Load System.Directoryservices" Try { [void] ([System.Reflection.Assembly]::LoadWithPartialName("System.Directoryservices.Protocols")) [void] ([System.Reflection.Assembly]::LoadWithPartialName("System.Directoryservices")) } Catch { Throw "Error loading assemblies: $($_.Exception.Message)" } Write-Verbose "Exit: Load System.Directoryservices" # Instantiate LdapDirectoryIdentifier Write-Verbose "Init: LdapDirectoryIdentifier" Try { $ldapDirectoryId = New-Object System.DirectoryServices.Protocols.LdapDirectoryIdentifier($Server, $Port) } Catch { Throw "Error instantiating LdapDirectoryIdentifier: $($_.Exception.Message)" } Write-Verbose "Exit: LdapDirectoryIdentifier" # Instantiate LdapConnection Write-Verbose "Init: LdapConnection" Try { If ($null -eq $Credential) { $ldapConnection = New-Object System.DirectoryServices.Protocols.LdapConnection($ldapDirectoryId) } Else { $ldapConnection = New-Object System.DirectoryServices.Protocols.LdapConnection($ldapDirectoryId, $Credential.GetNetworkCredential()) } } Catch { Throw "Error instantiating LdapConnection: $($_.Exception.Message)" } Write-Verbose "Exit: LdapConnection" # Instantiate SearchRequest Write-Verbose "Init: SearchRequest" Try { $searchReq = New-Object System.DirectoryServices.Protocols.SearchRequest $searchReq.DistinguishedName = $BaseDN $searchReq.Filter = $Filter $searchReq.Scope = [System.DirectoryServices.Protocols.SearchScope]($SearchScope) $searchReq.SizeLimit = $SizeLimit $searchReq.TimeLimit = [System.Timespan]::FromSeconds($TimeLimitSeconds) } Catch { Throw "Error instantiating SearchRequest: $($_.Exception.Message)" } Write-Verbose "Exit: SearchRequest" # Instantiate SendRequest Write-Verbose "Init: SendRequest" Try { $searchResponse = [System.DirectoryServices.Protocols.SearchResponse] $ldapConnection.SendRequest($searchReq) } Catch { Throw "There was an error searching Active Directory. Error Details: $($_.Exception.Message). `nInnerException: $($_.Exception.InnerException)" } Write-Verbose "Exit: SendRequest" $response = @($searchResponse.Entries) Write-Verbose "Results Count: $($response.Count)" Return $response } <# .Synopsis Generates a report of all certificates issued by the Hybrid Azure AD Foin feature which are stored in Active Directory Computer objects. .DESCRIPTION This tool checks for all certificates present in UserCertificate property of a Computer object in AD and, for each non-expired certificate present, validates if the certificate was issued for the Hybrid Azure AD join feature (i.e. Subject Name is CN={ObjectGUID}). Before version 1.4, Azure AD Connect would synchronize to Azure AD any Computer that contained at least one certificate but in Azure AD Connect version 1.4 and later, ADSync engine can identify Hybrid Azure AD join certificates and will "cloudfilter" (exclude) the computer object from synchronizing to Azure AD unless there's a valid Hybrid Azure AD join certificate present. Azure AD Device objects that were already synchronized to AD but do not have a valid Hybrid Azure AD join certificate will be deleted from Azure AD (CloudFiltered=TRUE) by AAD Connect. .EXAMPLE Export-ADSyncToolsHybridAadJoinReport -ObjectDN 'CN=Computer1,OU=SYNC,DC=Fabrikam,DC=com' .EXAMPLE Export-ADSyncToolsHybridAadJoinReport -BaseDN 'OU=SYNC,DC=Fabrikam,DC=com' -Filename "MyHybridAzureADjoinReport.csv" -Verbose .LINK More Information: https://docs.microsoft.com/en-us/troubleshoot/azure/active-directory/reference-connect-device-disappearance #> Function Export-ADSyncToolsHybridAadJoinReport { [CmdletBinding()] Param ( # Computer object's DistinguishedName [Parameter(ParameterSetName='SingleObject', Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [String] $ObjectDN, # AD OrganizationalUnit [Parameter(ParameterSetName='MultipleObjects', Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [String] $BaseDN, # Output CSV filename (optional) [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, Position=1)] [String] $Filename ) Import-ADSyncToolsActiveDirectoryModule # Generate Output filename if not provided If ($Filename -eq "") { $Filename = [string] "$([string] $(Get-Date -Format yyyyMMddHHmmss))_ADSyncAADHybridJoinCertificateReport.csv" } Write-Verbose "Output filename: '$Filename'" # Read AD object(s) If ($PSCmdlet.ParameterSetName -eq 'SingleObject') { $directoryObjs = @(Get-ADObject $ObjectDN -Properties UserCertificate) Write-Verbose "Starting report for a single object '$ObjectDN'" } Else { $directoryObjs = @(Get-ADObject -Filter { ObjectClass -like 'computer' } -SearchBase $BaseDN -Properties UserCertificate) Write-Verbose "Starting report for $($directoryObjs.Count) computer objects in '$BaseDN'" } If ($directoryObjs.Count -gt 0) { Write-Host "Processing $($directoryObjs.Count) directory object(s). Please wait..." # Check Certificates on each AD Object $results = @() ForEach ($obj in $directoryObjs) { # Read UserCertificate multi-value property $objDN = [string] $obj.DistinguishedName $objectGuid = [string] ($obj.ObjectGUID).Guid $userCertificateList = @($obj.UserCertificate) $validEntries = @() $totalEntriesCount = $userCertificateList.Count Write-verbose "'$objDN' ObjectGUID: $objectGuid" Write-verbose "'$objDN' has $totalEntriesCount entries in UserCertificate property." If ($totalEntriesCount -eq 0) { Write-verbose "'$objDN' has no Certificates - Skipped." Continue } # Check each UserCertificate entry and build array of valid certs ForEach($entry in $userCertificateList) { Try { $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2] $entry } Catch { Write-verbose "'$objDN' has an invalid Certificate!" Continue } Write-verbose "'$objDN' has a Certificate with Subject: $($cert.Subject); Thumbprint:$($cert.Thumbprint)." $validEntries += $cert } $validEntriesCount = $validEntries.Count Write-verbose "'$objDN' has a total of $validEntriesCount certificates (shown above)." # Get non-expired Certs (Valid Certificates) $validCerts = @($validEntries | Where-Object {$_.NotAfter -ge (Get-Date)}) $validCertsCount = $validCerts.Count Write-verbose "'$objDN' has $validCertsCount valid certificates (not-expired)." # Check for AAD Hybrid Join Certificates $hybridJoinCerts = @() $hybridJoinCertsThumbprints = [string] "|" ForEach ($cert in $validCerts) { $certSubjectName = $cert.Subject If ($certSubjectName.StartsWith($("CN=$objectGuid")) -or $certSubjectName.StartsWith($("CN={$objectGuid}"))) { $hybridJoinCerts += $cert $hybridJoinCertsThumbprints += [string] $($cert.Thumbprint) + '|' } } $hybridJoinCertsCount = $hybridJoinCerts.Count If ($hybridJoinCertsCount -gt 0) { $cloudFiltered = 'FALSE' Write-verbose "'$objDN' has $hybridJoinCertsCount AAD Hybrid Join Certificates with Thumbprints: $hybridJoinCertsThumbprints (cloudFiltered=FALSE)" } Else { $cloudFiltered = 'TRUE' Write-verbose "'$objDN' has no AAD Hybrid Join Certificates (cloudFiltered=TRUE)." } # Save results $r = "" | Select ObjectDN, ObjectGUID, TotalEntriesCount, CertsCount, ValidCertsCount, HybridJoinCertsCount, CloudFiltered $r.ObjectDN = $objDN $r.ObjectGUID = $objectGuid $r.TotalEntriesCount = $totalEntriesCount $r.CertsCount = $validEntriesCount $r.ValidCertsCount = $validCertsCount $r.HybridJoinCertsCount = $hybridJoinCertsCount $r.CloudFiltered = $cloudFiltered $results += $r } If ($results.Count -gt 0) { # Export results to CSV Try { $results | Export-Csv $Filename -NoTypeInformation -Delimiter ';' Write-Host "Exported Hybrid Azure AD Domain Join Certificate Report to '$Filename'.`n" -ForegroundColor Cyan } Catch { Throw "There was an error saving the file '$Filename': $($_.Exception.Message)" } } Else { Write-Host "No Hybrid Azure AD Join certificates found." -ForegroundColor Cyan } } Else { Write-Host "No Computer objects found." -ForegroundColor Cyan } } <# .Synopsis Gets the current AD DS Connector account(s) configured in Azure AD Connect .DESCRIPTION This function outputs AD DS Connector account(s) from the connectivity parameters configured in Azure AD Connect .EXAMPLE Get-ADSyncToolsADconnectorAccount #> Function Get-ADSyncToolsADconnectorAccount { [CmdletBinding()] Param() Write-Verbose "Enter: Get-ADSyncToolsADconnectorAccount" IsAADConnectPresent # Get AD Connectors Try { $adConnectors = Get-ADSyncConnector -ErrorAction Stop | Where-Object {$_.ConnectorTypeName -eq "AD"} } Catch { Throw "Failure getting ADSync Connectors: $($_.Exception.Message)" } # Get AD Connectivity Parameters $ADconnectorAccount = @() ForEach ($connector in $ADConnectors) { $connectorForestName = $connector.ConnectivityParameters | Where-Object {$_.Name -eq "forest-name"} $connectorAccountDomain = $connector.ConnectivityParameters | Where-Object {$_.Name -like "forest-login-domain"} $connectorAccountName = $connector.ConnectivityParameters | Where-Object {$_.Name -like "forest-login-user"} $row = "" | Select Name,Forest,Domain,Username $row.Name = $connector.Name $row.Forest = $connectorForestName.Value $row.Domain = $connectorAccountDomain.Value $row.Username = $connectorAccountName.Value $ADconnectorAccount += $row } Write-Verbose "Exit: Get-ADSyncToolsADconnectorAccount" Return $ADconnectorAccount } <# .Synopsis Gets the current ADSync service account configured for Azure AD Connect .DESCRIPTION This function outputs the account used by Microsoft Azure AD Sync (ADSync) service .EXAMPLE Get-ADSyncToolsADconnectorAccount #> Function Get-ADSyncToolsServiceAccount { [CmdletBinding()] Param() Write-Verbose "Enter: Get-ADSyncToolsServiceAccount" # Get ADSync Service Account from Windows services Try { $cimService = Get-CimInstance -ClassName CIM_Service -ErrorAction Stop | Where Name -eq 'ADSync'| Select Name, StartMode, StartName } Catch { Throw "Failure getting CimInstance services information: $($_.Exception.Message)" } If ([string]::IsNullOrEmpty($cimService)) { Throw "Cannot find 'Microsoft Azure AD Sync' (ADSync) service." } # Check service account type (Domain account / VSA / MSA / gMSA) If ($cimService.StartName[$cimService.StartName.Length-1] -eq '$') { $accountType = "ManagedAccount" } ElseIf ($cimService.StartName -eq "NT SERVICE\ADSync") { $accountType = "VSA" } ElseIf ($($cimService.StartName) -match $netbiosDomainRegex) { $accountType = "DomainAccount" } $serviceAccount = "" | select ServiceName,StartMode,ServiceLogOnAs,AccountType $serviceAccount.ServiceName = $cimService.Name $serviceAccount.StartMode = $cimService.StartMode $serviceAccount.ServiceLogOnAs = $cimService.StartName $serviceAccount.AccountType = $accountType Write-Verbose "Exit: Get-ADSyncToolsServiceAccount" Return $serviceAccount } <# .Synopsis Diagnostic tool for AADConnect Password Writeback feature .DESCRIPTION Tests a Password Writeback operation (Password Reset) for a given AD Connector Account and a target user account. Sources: NetUserGetInfo function (lmaccess.h) - https://docs.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetinfo USER_INFO_1 structure (lmaccess.h) - https://docs.microsoft.com/en-us/windows/win32/api/lmaccess/ns-lmaccess-user_info_1 IADsUser::SetPassword method (iads.h) - https://docs.microsoft.com/en-us/windows/win32/api/iads/nf-iads-iadsuser-setpassword Protected Accounts and Groups in Active Directory - https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/appendix-c--protected-accounts-and-groups-in-active-directory .EXAMPLE Test-ADSyncToolsPasswordWriteback -Credential $(Get-Credential) -DomainName "Contoso.com" -TargetUser 'user1' .EXAMPLE Test-ADSyncToolsPasswordWriteback -Credential $(Get-Credential) -DomainName "Contoso.com" -TargetUser 'user1' -Server DomainController1.contoso.com .EXAMPLE $creds = Get-Credential $creds | Test-ADSyncToolsPasswordWriteback -DomainName "Contoso.com" -TargetUser 'username1' #> Function Test-ADSyncToolsPasswordWriteback { [CmdletBinding()] Param ( # AD Connector Account credentials [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [PSCredential] $Credential, # Target FQDN (e.g. Contoso.com) [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] [string] $DomainName, # Target Username (sAMAccountName) [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=2)] [string] $TargetUser, # Target Domain Controller name [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=3)] [string] $Server, [Parameter(Mandatory=$false, Position=4)] [switch] $NewPasswordPrompt = $False ) # BEGIN $ErrorActionPreference = 'Stop' $DS_PDC_REQUIRED = 0x00000080 $DS_IS_DNS_NAME = 0x00020000 $DS_RETURN_DNS_NAME = 0x40000000 $ImpersonatedUser = @{} $tokenHandle = 0 $dcBuffer = 0 # Set AD Connector Account Credentials $Username = $Domain = $Password = $null Get-Variable Username, Domain, Password | ForEach-Object { Set-Variable $_.Name -Value $Credential.GetNetworkCredential().$($_.Name) } # PROCESS # Impersonate AD Connector Account Credentials Write-Host "Attempting to impersonate user '$Username'..." Write-Verbose "Attempting LogonUser..." $returnValue = [NetApi32]::LogonUser($Username, $Domain, $Password, 2, 3, [ref]$tokenHandle) $Domain = $Password = $Credential = $null if ($returnValue -eq $false) { $errCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error(); Write-Host "Impersonate-User failed a call to LogonUser with error code: $errCode" Throw [System.ComponentModel.Win32Exception]$errCode } Write-Verbose "Attempting ImpersonationContext..." $ImpersonatedUser.ImpersonationContext = [System.Security.Principal.WindowsIdentity]::Impersonate($tokenHandle) [void][NetApi32]::CloseHandle($tokenHandle) Write-Host "Impersonating user $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) ..." -ForegroundColor Green # Acquiring target DC If ($Server -eq '') { Write-Verbose "Attempting DsGetDcName for target Domain '$DomainName'..." Try { $result = [NetApi32]::DsGetDcName("", $DomainName, 0, "", $DS_PDC_REQUIRED -bor $DS_IS_DNS_NAME -bor $DS_RETURN_DNS_NAME , [ref]$dcBuffer) $dcInfo = [Runtime.InteropServices.Marshal]::PtrToStructure($dcBuffer, [Type] ("NetApi32+DomainControllerInfo" -as [Type])) } Catch { Cleanup -Buffer $outBuffer -User $ImpersonatedUser Throw "DsGetDcName failure: $($_.Exception.Message) `nInnerException: $($_.Exception.InnerException)" } If ($dcInfo -eq $null) { Cleanup -Buffer $outBuffer -User $ImpersonatedUser Throw "Domain '$DomainName' not found." } [Void] [NetApi32]::NetApiBufferFree($dcBuffer) # DC Information $dcName = $dcInfo.DomainControllerName If ($dcName.length -gt 0 -and $dcName.StartsWith(".")) { $dcName = $dcName.Substring(1); } If($dcName.length -gt 0 -and $dcName.StartsWith("\")) { $dcName = $dcName.Substring(1); } If ($dcName.length -gt 0 -and $dcName.StartsWith("\")) { $dcName = $dcName.Substring(1); } Write-Host "DC Information: " -NoNewline $dcInfo } Else { $dcName = $Server } # Get target user info Write-Verbose "Attempting NetUserGetInfo for target user '$TargetUser' against DC '$dcName'..." Try { $outBuffer = 0 $result = [NetApi32]::NetUserGetInfo($dcName, $TargetUser, 1, [ref]$outBuffer) } Catch { Cleanup -Buffer $outBuffer -User $ImpersonatedUser Throw "NetUserGetInfo failure: $($_.Exception.Message) `nInnerException: $($_.Exception.InnerException)" } If ($result -ne 0) { Write-Host "Impersonate-User failed with error code: $result" Cleanup -Buffer $outBuffer -User $ImpersonatedUser Throw [System.ComponentModel.Win32Exception]$result } Write-Verbose "Retrieving NetUserGetInfo for target user '$TargetUser'..." $userInfo = [Runtime.InteropServices.Marshal]::PtrToStructure($outBuffer, [Type] ("NetApi32+USER_INFO_1" -as [Type])) Write-Host "`nTarget User Information" #"sHome_Dir : $($userInfo.sHome_Dir )" #"sPassword : $($userInfo.sPassword )" #"sScript_Path : $($userInfo.sScript_Path )" "sUsername : $($userInfo.sUsername )" "uiFlags : $($userInfo.uiFlags )" "uiPasswordAge : $($userInfo.uiPasswordAge)" "uiPriv : $($userInfo.uiPriv)" Write-Host "`nUserAccountControl Flags on target user '$TargetUser': " $uiFlags = "" | select PASSWD_CANT_CHANGE, ` DONT_EXPIRE_PASSWD, ` MNS_LOGON_ACCOUNT, ` SMARTCARD_REQUIRED, ` TRUSTED_FOR_DELEGATION, ` NOT_DELEGATED, ` USE_DES_KEY_ONLY, ` DONT_REQUIRE_PREAUTH, ` PASSWORD_EXPIRED, ` TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, ` NO_AUTH_DATA_REQUIRED, ` PARTIAL_SECRETS_ACCOUNT, ` USE_AES_KEYS, ` TEMP_DUPLICATE_ACCOUNT, ` NORMAL_ACCOUNT, ` INTERDOMAIN_TRUST_ACCOUNT, ` WORKSTATION_TRUST_ACCOUNT, ` SERVER_TRUST_ACCOUNT $accountFlags = Get-Member -InputObject $uiFlags -MemberType Properties | select -ExpandProperty Name ForEach ($f in $accountFlags) { [bool] $uiFlags.$f = $userInfo.uiFlags -band [NetApi32]::$("UF_"+ $f) } $uiFlags # Result If (($userInfo.uiFlags -band [NetApi32]::UF_PASSWD_CANT_CHANGE) -ne 0) { Cleanup -Buffer $outBuffer -User $ImpersonatedUser Throw "ERROR_ACCESS_DENIED: Can't change password for the target user '$TargetUser' (UF_PASSWD_CANT_CHANGE flag)" } Else { Write-Host "Impersonate-User executed successfully." } Try { # Reset Password for user Set-TargetUserPassword -DomainName $DomainName -SAMAccountName $TargetUser -NewPasswordPrompt:$([bool]$NewPasswordPrompt) } Catch { Cleanup -Buffer $outBuffer -User $ImpersonatedUser Throw "Set password failure: $($_.Exception.Message)" } # END Cleanup -Buffer $outBuffer -User $ImpersonatedUser Write-Host "Security context returned to previous user $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" } Function Set-TargetUserPassword { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [string] $DomainName, [Parameter(Mandatory=$true)] [string] $SAMAccountName, [Parameter(Mandatory=$false)] [switch] $NewPasswordPrompt ) # Check security policy "\Network access: Restrict clients allowed to make remote calls to SAM" Write-Host "Checking Security policy 'Network access: Restrict clients allowed to make remote calls to SAM' (aka. RestrictRemoteSAM) under 'Computer Configuration|Windows Settings|Security Settings|Local Policies|Security Options'..." $restrictRemoteSam = Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Control\Lsa | select -ExpandProperty RestrictRemoteSAM -ErrorAction Ignore If ($restrictRemoteSam -ne $null) { Write-Warning "RestrictRemoteSAM policy is present. Password Writeback might not work if the AD DS Connector account is not allowed in RestrictRemoteSAM policy." } Write-Host "RestrictRemoteSAM policy must also be disabled on the DC side. Type 'Get-ItemProperty -Path HKLM:\System\CurrentControlSet\Control\Lsa | select RestrictRemoteSAM' on the target DC to confirm if RestrictRemoteSAM policy is present." -ForegroundColor Yellow # Get target user from AD Write-Verbose "Seaching for target user '$SAMAccountName'..." $targetUser = Find-TargetUser -DomainName $DomainName -SAMAccountName $SAMAccountName If ($targetUser -eq $null) { Write-Error "User '$SAMAccountName' not found in domain '$DomainName'." Return } # Check if is a Protected Account (adminCount == 1) If ($targetUser.Properties.admincount -eq '1') { Write-Error "User '$($targetUser.Path)' is a Protected Account (adminCount == 1)." Return } # Check if AD permissions inheritance is disabled $adObject = $targetUser.GetDirectoryEntry() If ($adObject.ObjectSecurity.AreAccessRulesProtected) { Write-Error "User '$($targetUser.Path)' has AD permissions inheritance disabled." Return } Write-Verbose "Target user DN: $($targetUser.Path)" Try { $oTargetUser = [adsi] $targetUser.Path } Catch { Write-Error "ADSI failure: $($_.Exception.Message) `nInnerException: $($_.Exception.InnerException)" Return } If ($NewPasswordPrompt) { $pwdStr1 = Read-Host "New Password" -AsSecureString $pwdStr2 = Read-Host "Confirm Password" -AsSecureString $pwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwdStr1)) If ([string]::IsNullOrEmpty($pwd)) { Throw "Invalid password. Please try again." } If ([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwdStr2)) -ne $pwd) { Throw "Passwords don't match. Please try again." } } Else { # Use random string $pwd = "O6FYO0&rRvJG5tzlOw55" } Write-Host "`nAttempting to reset password for user '$SAMAccountName'..." Try { $oTargetUser.psbase.invoke('SetPassword',$pwd) $oTargetUser.psbase.CommitChanges() } Catch { Write-Error "ADSI failure: $($_.Exception.Message) `nInnerException: $($_.Exception.InnerException)" Return } Write-Host "`nPassword Reset for user '$SAMAccountName' terminated successfully..." -ForegroundColor Green # Wait for AD replications Start-Sleep -Seconds 5 # Get Last password changed time: $targetUser = Find-TargetUser -DomainName $DomainName -SAMAccountName $SAMAccountName Try { [string] $pwdLastChanged = ([datetime]::FromFileTime($targetUser.Properties.pwdlastset[0])).DateTime } Catch { [string] $pwdLastChanged = $targetUser.Properties.pwdlastset } Write-Host "`nLast password changed time: $pwdLastChanged" } #TODO: replace with Search-ADSyncToolsADobject Function Find-TargetUser { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [string] $DomainName, [Parameter(Mandatory=$true)] [string] $SAMAccountName ) $root = [ADSI]'' $searcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList ($DomainName) $searcher.Filter = "(&(objectClass=User)(sAMAccountName=$SAMAccountName))" $user = $searcher.FindAll() Return $user } Function Cleanup { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [object] $Buffer, [Parameter(Mandatory=$true)] [object] $User ) Write-Verbose "Cleaning up..." [void][NetApi32]::NetApiBufferFree($buffer) $user.ImpersonationContext.Undo() $user.ImpersonationContext.Dispose() } <# .Synopsis Automates troubleshooting with Single Object Sync tool .DESCRIPTION Run the Single Object Sync tool from ADSyncDiagnostics and saves the results to a json file in the current directory. .EXAMPLE Start-ADSyncToolsSingleObjectSync -DistinguishedName "CN=User1,OU=Corp,DC=Contoso,DC=com" #> Function Start-ADSyncToolsSingleObjectSync { [CmdletBinding()] Param ( # DistinguishedName of the target object [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [string] $DistinguishedName ) IsAADConnectPresent -MinVersion '1.6.2.4' Write-Warning "This operation will temporarily stop the Sync Scheduler. Do you want to continue?" -WarningAction Inquire If (IsSyncCycleRunning) { Throw "Azure AD Connect is currently running a sync scheduler." } Set-ADSyncScheduler -SyncCycleEnabled $false $adSyncLocation = Get-ADSyncToolsADsyncFolder If (-not [string]::IsNullOrEmpty($adSyncLocation)) { Try { Import-Module "$($adSyncLocation)Bin\ADSyncDiagnostics\ADSyncDiagnostics.psm1" -ErrorAction Stop } Catch { Set-ADSyncScheduler -SyncCycleEnabled $true Throw "Cannot import ADSyncDiagnostics Module: $($_.Exception.InnerException)" } $reportFilename = "C:\ProgramData\AADConnect\ADSyncObjectDiagnostics\ADSyncSingleObjectSyncResult-$(Get-Date -Format yyyyMMddHHmmss)" $result = Invoke-ADSyncSingleObjectSync -DistinguishedName $DistinguishedName if ($result) { $result | Out-File -FilePath "$($reportFilename).json" Write-Host "`nSingle Object Sync report saved in '$reportFilename'`n" -ForegroundColor Green } } Set-ADSyncScheduler -SyncCycleEnabled $true } #endregion #======================================================================================= #======================================================================================= #region Automated Logman (ETW) tracing #======================================================================================= Function Checkpoint-ADSyncToolsLogmanTrace { [CmdletBinding()] Param ( # Command (Init, Start, Stop) [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [ValidateSet('Init', 'Start', 'Stop')] [string] $Command ) # Get current datetime $currentDateTime = Get-Date $currentTime = Get-Date $currentDateTime -Format yyyyMMdd-HHmmss $currentTimeUTC = Get-Date ($currentDateTime.ToUniversalTime()) -Format "yyyy-MM-dd HH:mm:ss" $logName = 'ADSyncTools-SyncTrace' if ($Command -eq 'Init') { # Save old log file Rename-Item "$logName.log" "$($logName)_$currentTime.log" -ErrorAction Ignore # Init log file "DateTime,DateTime (UTC),Status" | Out-File -FilePath "$logName.log" [bool] $script:ADSyncToolsLogmanLogInit = $true } If (-not $script:ADSyncToolsLogmanLogInit) { Checkpoint-ADSyncToolsLogmanTrace -Command Init } # Save log entry "$currentTime,$currentTimeUTC (UTC),$Command" | Out-File -FilePath "$logName.log" -Append # Return filename for ETL trace if ($Command -eq 'Start') { Return "$($logName)_$currentTime.etl" } } Function Trace-ADSyncToolsLogmanTrace { [CmdletBinding()] Param ( # No output [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=0)] [switch] $NullOutput ) $filename = Checkpoint-ADSyncToolsLogmanTrace 'Start' # start logman $cmd = 'logman.exe' $arg1 = 'start' $arg2 = 'mysession' $arg3 = '-p' $arg4 = '{cec61b36-75f2-44b3-ba80-177955c0db12}' $arg5 = '-o' $arg6 = $filename $arg7 = '-ets' Write-Verbose "Starting trace: $cmd $arg1 $arg2 $arg3 $arg4 $arg5 $arg6 $arg7" $output = & $cmd $arg1 $arg2 $arg3 $arg4 $arg5 $arg6 $arg7 If ($LASTEXITCODE -ne 0) { If ($output -like "*Data Collector Set already exists*") { $output += "Use 'Stop-ADSyncToolsLogmanTrace' to stop the current trace." } Throw $output } If (-not $NullOutput) { $output } } <# .Synopsis Stops the automated ETW trace on each synchronization cycle .DESCRIPTION To be used when ETW tracing is already running. Check 'Start-ADSyncToolsLogmanTrace' help for more details: Get-Help Start-ADSyncToolsLogmanTrace -Full .EXAMPLE Stop-ADSyncToolsLogmanTrace #> Function Stop-ADSyncToolsLogmanTrace { [CmdletBinding()] Param ( # No output [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=0)] [switch] $NullOutput ) Checkpoint-ADSyncToolsLogmanTrace 'Stop' #end logman $cmd = 'logman.exe' $arg1 = 'stop' $arg2 = 'mysession' $arg7 = '-ets' Write-Verbose "Stopping trace: $cmd $arg1 $arg2 $arg7" $output = & $cmd $arg1 $arg2 $arg7 If ($LASTEXITCODE -ne 0) { Throw $output } If (-not $NullOutput) { $output } } <# .Synopsis Starts an automated ETW trace on each synchronization cycle .DESCRIPTION When using ETW tracing to troubleshoot synchronization issues on a large deployment, ETL files can grow rapidly. With this tool, you can leave ETW tracing running but a separated ETL file will be created for every sync cycle. The tool will also create a log file 'ADSyncTools-SyncTrace.log' on the same folder where you can check for ETW tracing activity. It is recommended that you go to a temporary folder as all the files will be created on the current directory. To use this cmdlet, you need to first configure miiserver.exe.config file for verbose logging by following these instructions: 1. Edit the file "C:\Program Files\Microsoft Azure AD Sync\Bin\miiserver.exe.config" 2. For each source that you want to trace, set the switchValue to Verbose level: switchValue="Verbose" 3. Restart the ADSync Service to pick up the new config settings 4. Open a PowerShell session with "Run As Administrator" 5. Go to the target folder where the trace files will be created, e.g.: C:\Temp\ADSyncToolsLogmanTrace\ 5. Start tracing the sync cycles with: Start-ADSyncToolsLogmanTrace 6. Leave the script running while ETW traces are being captured. 7. When you're done capturing ETW traces, press CTRL+C to stop monitoring sync cycles and stop the current running trace with: Stop-ADSyncToolsLogmanTrace .EXAMPLE Start-ADSyncToolsLogmanTrace .EXAMPLE Start-ADSyncToolsLogmanTrace -SleepTimerSecs 10 #> Function Start-ADSyncToolsLogmanTrace { [CmdletBinding()] Param( # Whether to trace sync cycles continuously [Parameter(Mandatory=$false, Position=0)] [switch] $SyncSycleMonitoring, # Wait time to check for the next sync cycle [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=1)] [int] $SleepTimerSecs = 60 ) $VerboseLevel = Confirm-ADSyncToolsLogmanTraceLevel If ($VerboseLevel) { $script:ADSyncToolsLogmanLogInit = $false If ($SyncSycleMonitoring) { Write-Host "Press CTRL+C any time to stop monitoring Sync Scheduler and type 'Stop-ADSyncToolsLogmanTrace' to stop the current trace." -ForegroundColor Cyan Do { $currentSyncCycleTime = Get-Date $((Get-ADSyncScheduler).NextSyncCycleStartTimeInUTC) if ($currentSyncCycleTime -le $startedSyncCycleTime) { Write-Verbose "Waiting for next Sync cycle (Sleeping)..." Start-Sleep -Seconds $SleepTimerSecs } Else { # Start new ETl trace Write-Verbose "Sync cycle started (Starting ETW trace)..." If ($startedSyncCycleTime -ne $null) { Write-Verbose "Sync cycle ended (Stopping ETW trace)..." Stop-ADSyncToolsLogmanTrace -NullOutput } Trace-ADSyncToolsLogmanTrace -NullOutput $startedSyncCycleTime = $currentSyncCycleTime Write-Verbose "startedSyncCycleTime = currentSyncCycleTime = $currentSyncCycleTime" } } While ($true) } Else { Trace-ADSyncToolsLogmanTrace Write-Host "Tracing has started, you can now reproduce the issue and then use 'Stop-ADSyncToolsLogmanTrace' to stop the current trace.`n" -ForegroundColor Green } } } <# .Synopsis Searches a given SourceObjectId in ADSync database and returns the respective object name e.g. DistinguishedName #> Function Resolve-ADSyncToolsObjectName { [CmdletBinding()] [Alias()] [OutputType([string])] Param ( # In-Memory cache with ObjectId mappings [Parameter(Mandatory=$true, Position=0)] [ref] $CacheTable, # Source of the object to lookup (ConnectorSpace / Metaverse) [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)] [ValidateSet('ConnectorSpace','Metaverse')] $Source, # Source object Identifier (GUID) [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=2)] $SourceObjectId, # Include the original SourceObjectId in the results [Parameter(Mandatory=$false, Position=3)] [switch] $IncludeGuid = $false ) Write-Verbose "Entering: Resolve Object: $source | $SourceObjectId" # Check if mapping already exists $findEntry = $CacheTable.Value[$SourceObjectId] If ($null -eq $findEntry) { # Find object in database Write-Verbose "Object not found in cache: $SourceObjectId" $objectName = $null switch ($Source) { 'ConnectorSpace' { Try { $csObj = Get-ADSyncCSObject -Identifier $SourceObjectId -ErrorAction Stop } Catch { If ($_.Exception.Message -match 'Invalid connector space object xml') { Write-Verbose "Object not found in Connector Space, searching in MV..." Return (Resolve-ADSyncToolsObjectName -CacheTable $CacheTable -Source Metaverse -SourceObjectId $SourceObjectId) } Else { Write-Verbose "Object not found in Connector Space. Error Details: $($_.Exception.Message)" Write-Error "Error occurred finding SourceObjectId ' $SourceObjectId' in '$Source'. Error Details: $($_.Exception.Message)" $objectName = "SearchError" $IncludeGuid = $true } } If ([string]::IsNullOrEmpty($objectName)) { $objectName = Get-ADSyncToolsObjectNameFromCS -CsObj $csObj # Note: no [ref] here because $CacheTable is already a [ref] inside this function Add-ADSyncToolsLogmanTraceCache -CacheTable $CacheTable -Name $objectName -Value $SourceObjectId } } 'Metaverse' { Try { $mvObj = Get-ADSyncMVObject -Identifier $SourceObjectId -ErrorAction Stop } Catch { Write-Warning "Object '$SourceObjectId' not found in ConnectorSpace/Metaverse. Details: $($_.Exception.Message)" $objectName = "ObjectNotFound" $IncludeGuid = $true } If ([string]::IsNullOrEmpty($objectName)) { $objectName = Get-ADSyncToolsObjectNameFromMV -MvObj $mvObj } Add-ADSyncToolsLogmanTraceCache -CacheTable $CacheTable -Name $objectName -Value $SourceObjectId } Default { Throw "Unexpected parameter value. Please provide a valid 'Source' value." } } } Else { $objectName = $findEntry Write-Verbose "Object mapping already in memory cache: $SourceObjectId ; $findEntry" } If ($IncludeGuid) { $objectName += "{$SourceObjectId}" } Write-Verbose "Exiting: Resolve Object: $source | $SourceObjectId | $objectName" Return $objectName } Function Get-ADSyncToolsObjectNameFromMV { [CmdletBinding()] [Alias()] Param ( # Metaverse Object [Parameter(Mandatory=$true, Position=0)] $MvObj ) Write-Verbose "Entering: Get-ADSyncToolsObjectNameFromMV" # Metaverse object attribute list $AttributeNamePriority = @( 'distinguishedName', # 1st priority 'userPrincipalName', # 2nd, if a user 'cn', # 3rd, if no distinguishedName (same as 'commonName') 'mailNickname', # 4th, if a GroupWriteback (same as 'alias') 'cloudAnchor' # finally, as a last resort ) # Get attribute value from priority list [string] $objectName = $null ForEach ($a in $AttributeNamePriority) { $objectName = $MvObj.Attributes[$a].Values If (-not ([string]::IsNullOrEmpty($objectName))) { Break } } $namePrefix = "MV|'" If ([string]::IsNullOrEmpty($objectName)) { $objectName = "AttributeValueNotFound" } # Get MV object type Try { [xml] $objXml = $MvObj.SerializedXml } Catch { Write-Error "There was an error getting the object type. Error Details: $($_.Exception.Message)" } $objType = $objXml.'mv-objects'.'mv-object'.entry.'primary-objectclass'.'#text' Write-Verbose "Exiting: Get-ADSyncToolsObjectNameFromMV" Return ($namePrefix + $objectName + "'(" + $objType + ")") } Function Get-ADSyncToolsObjectNameFromCS { [CmdletBinding()] [Alias()] Param ( # Connector Space Object [Parameter(Mandatory=$true, Position=0)] $CsObj ) Write-Verbose "Entering: Get-ADSyncToolsObjectNameFromCS" If ($CsObj.ConnectorId -eq 'b891884f-051e-4a83-95af-2544101c9083') { # AAD CS object Write-Verbose "AAD CS Object." $AttributeNamePriority = @( 'onPremisesDistinguishedName', # 1st priority 'userPrincipalName', # 2nd, if a user 'commonName', # 3rd, if no onPremisesDistinguishedName 'alias', # 4th, if a GroupWriteback 'cloudAnchor' # finally, as a last resort (if a group) ) # Get attribute value from priority list [string] $objectName = $null ForEach ($a in $AttributeNamePriority) { $objectName = $CsObj.Attributes[$a].Values Write-Verbose "Attribute value from '$a' = '$objectName'" If (-not ([string]::IsNullOrEmpty($objectName))) { Break } } $namePrefix = "AADCS|'" } Else { # AD CS object - Should always have a DistinguishedName Write-Verbose "AD CS Object." $objectName = $CsObj.DistinguishedName $namePrefix = "ADCS|'" } If ([string]::IsNullOrEmpty($objectName)) { $objectName = "AttributeValueNotFound" } # Get MV object type $objType = $CsObj.ObjectType Write-Verbose "Exiting: Get-ADSyncToolsObjectNameFromCS" Return ($namePrefix + $objectName + "'(" + $objType + ")") } Function Initialize-ADSyncToolsLogmanTraceCache { [CmdletBinding()] [Alias()] Param ( # In-Memory cache with ObjectId mappings [Parameter(Mandatory=$true, Position=0)] [ref] $CacheTable, # Disk cache filename [Parameter(Mandatory=$true, Position=1)] [string] $CacheFilename ) Write-Verbose "Entering Cache Init: $CacheFilename" If (-not (Test-Path $CacheFilename)) { Write-Verbose "Cache not found, creating new file." # Create disk cache file Set-Content $CacheFilename "guid;name" # Add Null Guid 00000000-0000-0000-0000-000000000000 Add-Content $CacheFilename "00000000-0000-0000-0000-000000000000;00000000-0000-0000-0000-000000000000" # Add ConnectorIds Get-ADSyncConnector | select Name,Identifier | %{ Add-Content $CacheFilename "$($_.Identifier);Connector|$($_.Name)" } # Add SyncRules Get-ADSyncRule | select Name,Identifier | %{ Add-Content $CacheFilename "$($_.InternalId);SyncRule|$($_.Name)" } } # Import disk cache to memory Import-CSV -Path $CacheFilename -Delimiter ';' | %{ $CacheTable.Value.Add($_.guid,$_.name) } Write-Verbose "Exiting Cache Init: $($CacheTable.Value.Count) objects in cache." } Function Add-ADSyncToolsLogmanTraceCache { [CmdletBinding()] [Alias()] Param ( # In-Memory cache with ObjectId mappings [Parameter(Mandatory=$true, Position=0)] [ref] $CacheTable, # Name of the object to add [Parameter(Mandatory=$true, Position=1)] [string] $Name, # Guid Value of the object to add [Parameter(Mandatory=$true, Position=2)] [string] $Value ) Write-Verbose "Entering: Add Object to cache." # Check if mapping already exists $findEntry = $CacheTable.Value[$Value] If ($findEntry -eq $null) { Write-Verbose "Adding object mapping to memory cache: $Value ; $Name" $CacheTable.Value.Add($Value, $Name) } Else { Write-Verbose "Object mapping already in memory cache: $Value ; $Name" } Write-Verbose "Exiting: Add Object to cache." } Function Save-ADSyncToolsLogmanTraceCache { [CmdletBinding()] [Alias()] Param ( # In-Memory cache with ObjectId mappings [Parameter(Mandatory=$true, Position=0)] [ref] $CacheTable, # Disk cache filename [Parameter(Mandatory=$true, Position=1)] [string] $CacheFilename ) Write-Verbose "Entering: Save cache to disk: $CacheFilename" Try { # Export ObjectId mappings to disk cache file $CacheTable.Value.GetEnumerator() | Select-Object -Property @{Name='guid';Expression={$_.Key}}, @{Name='name';Expression={$_.Value}} | Export-Csv -NoTypeInformation -Path $CacheFilename -Delimiter ';' } Catch { Throw "There was an error exporting CSV file. Error Details: $($_.Exception.Message)" } Write-Verbose "Exiting: Save cache to disk: $CacheFilename | $($CacheTable.Value.Count) entries" } Function Find-ADSyncToolsMiiserverExeConfig { [CmdletBinding()] [Alias()] Param () $configFilename = 'miiserver.exe.config' Try { $configFilePath = "$(Get-ADSyncToolsADsyncFolder)bin\$configFilename" Write-Verbose "$configFilename file: $configFilePath" } Catch { Throw "There was an error getting the '$configFilename' file. Error Details: $($_.Exception.Message)" } Return $configFilePath } Function Import-ADSyncToolsMiiserverExeConfig { [CmdletBinding()] [Alias()] Param () $configFilename = Find-ADSyncToolsMiiserverExeConfig Try { [xml] $configXml = Get-Content $configFilename -ErrorAction Stop Write-Verbose "$configFilename parsed." } Catch { Throw "There was an error parsing the '$configFilename' file. Error Details: $($_.Exception.Message)" } Return $configXml } Function Export-ADSyncToolsMiiserverExeConfig { [CmdletBinding()] [Alias()] Param ( # XML config data [Parameter(Mandatory=$true, Position=0)] [xml] $ConfigXml ) $currentConfigFile = Find-ADSyncToolsMiiserverExeConfig # Backup current config file $backupConfigFile = $currentConfigFile + "_$(Get-Date -Format yyyyMMdd-HHmmss).bak" Try { Rename-Item $currentConfigFile $backupConfigFile -ErrorAction Stop } Catch { Throw "There was an error saving a backup of the '$configFilename' file. Error Details: $($_.Exception.Message)" } # Save the new config Try { $ConfigXml.Save($currentConfigFile) } Catch { Throw "There was an error saving a backup of the '$configFilename' file. Error Details: $($_.Exception.Message)" } } <# .Synopsis Gets the current ETW trace level for SyncRulesPipeline debugging .DESCRIPTION You can use this function to check what is the current ETW trace level. .EXAMPLE Get-ADSyncToolsLogmanTraceLevel #> Function Get-ADSyncToolsLogmanTraceLevel { [CmdletBinding()] [Alias()] Param () [xml] $configXml = Import-ADSyncToolsMiiserverExeConfig Write-Verbose "Config data: $configXml" Return ($configXml.configuration.'system.diagnostics'.sources.source) } <# .Synopsis Sets the ETW trace level (Warning/Verbose) for SyncRulesPipeline debugging .DESCRIPTION By default ETW trace level of SyncRulesPipeline is 'Warning'. You can use this function to change ETW trace level to 'Verbose'. To check what is the current ETW trace level you can use 'Get-ADSyncToolsLogmanTraceLevel'. This funtion must be run in an elevated PowerShell window. .EXAMPLE Set-ADSyncToolsLogmanTraceLevel -Level 'Verbose' .EXAMPLE Set-ADSyncToolsLogmanTraceLevel -Level 'Warning' #> Function Set-ADSyncToolsLogmanTraceLevel { [CmdletBinding()] [Alias()] Param ( # Level of ETW traces produced by the different module sources in the SyncRulesPipeline (Default='Warning') [Parameter(Mandatory=$true, Position=0)] [ValidateSet('Warning','Verbose', IgnoreCase = $false)] $Level ) $verboseLevel = Confirm-ADSyncToolsLogmanTraceLevel -SkipWarning If (($verboseLevel -and $Level -eq 'Verbose') -or ((-not $verboseLevel) -and $Level -eq 'Warning')) { # Trace Level already set Write-Host "No changes needed in config file.`n" -ForegroundColor Cyan } Else { [xml] $configXml = Import-ADSyncToolsMiiserverExeConfig Write-Verbose "Config data: $configXml" Try { $configXml.configuration.'system.diagnostics'.sources.source | %{$_.switchValue = $Level} Write-Verbose "Config updated with level: $Level" } Catch { Throw "There was an error setting trace level from the '$configFilename' file. Error Details: $($_.Exception.Message)" } Export-ADSyncToolsMiiserverExeConfig -ConfigXml $configXml Write-Verbose "Config saved: $configXml" Write-Host "Trace level set to '$Level'.`n" -ForegroundColor Green $srvName = "'Microsoft Azure AD Sync' (ADsync) service" $title = "Changes to config file require $srvName restart.`n" $question = "Restart $srvName now?" $choices = '&Yes', '&No' $decision = $Host.UI.PromptForChoice($title, $question, $choices, 1) If ($decision -eq 0) { Try { Restart-Service ADSync -ErrorAction Stop If ($Level -eq 'Verbose') { Write-Host "Changes in config file applied successfully. Use 'Start-ADSyncToolsLogmanTrace' to start ETW tracing.`n" -ForegroundColor Green } Else { Write-Host "Changes in config file applied successfully.`n" -ForegroundColor Green } } Catch { Write-Error "There was an error restarting $srvName. Error Details: $($_.Exception.Message)" } } Else { Write-Host "Changes in config file are pending $srvName restart.`n" -ForegroundColor Cyan } } } Function Confirm-ADSyncToolsLogmanTraceLevel { [CmdletBinding()] [Alias()] Param ( # Supress warning [Parameter(Mandatory=$false, Position=0)] [switch] $SkipWarning ) $configXml = Import-ADSyncToolsMiiserverExeConfig $syncModules = $configXml.configuration.'system.diagnostics'.sources.source Write-Host "" Write-Host "Current config has the following trace level:" $syncModules | select name, switchValue | %{Write-Host "$($_.Name) = $($_.switchValue)"} Write-Host "" $verboseModules = $($syncModules | where {$_.switchValue -eq 'Verbose'}) If ($verboseModules.Count -lt 1) { # Sync Modules not set with verbose tracing If (-not $SkipWarning) { Write-Warning "Current config does not have 'Verbose' tracing enabled.`n" Write-Host "`nTo set the module sources in the SyncRulesPipeline for Verbose level use:" Write-Host " Set-ADSyncToolsLogmanTraceLevel -Level Verbose`n" } Return $false } Else { # Sync Modules set with verbose tracing Return $true } } <# .Synopsis Decodes an ETW trace for SyncRulesPipeline debugging into a CSV text file .DESCRIPTION IMPORTANT: This function must be executed on the same AADConnect server where the ETW trace was captured. This function takes an .ETL file containing a SyncRulesPipeline ETW trace and decodes it to a CSV file. .EXAMPLE Convert-ADSyncToolsLogmanTrace -Path '.\SyncEventTrace.etl' #> Function Convert-ADSyncToolsLogmanTrace { [CmdletBinding()] [Alias()] Param ( # Path to ETL trace file [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [string] $Path ) Try { $inputFile = Get-Item $Path -ErrorAction Stop } Catch { Throw "There was an error opening the file '$Path'. Error Details: $($_.Exception.Message)" } If ($inputFile.Extension -ne '.etl') { Throw "Invalid ETW trace file, please provide a '.etl' file to convert." } # Execute tracerpt # e.g.: tracerpt synctrace.etl -o SyncEventTrace.csv -of CSV [string] $outputFile = $Path + "-converted.csv" $cmd = 'tracerpt.exe' $arg1 = $Path $arg2 = '-o' $arg3 = $outputFile $arg4 = '-of' $arg5 = 'CSV' Write-Verbose "Starting trace: $cmd $arg1 $arg2 $arg3 $arg4 $arg5" & $cmd $arg1 $arg2 $arg3 $arg4 $arg5 $arg6 $arg7 " " If ($LASTEXITCODE -ne 0) { Throw "An unexpected error occurred." } } <# .Synopsis Decodes an ETW trace for SyncRulesPipeline debugging and translates the ObjectIds to object names .DESCRIPTION IMPORTANT: This function must be executed on the same AADConnect server where the ETW trace was captured. This function takes an .ETL file containing a SyncRulesPipeline ETW trace and decodes it to a CSV file. Same as Convert-ADSyncToolsLogmanTrace. Then, it parses the CSV file and translates every ObjectId (GUID) to the respective object name, i.e. DistinguishedName by querying the ADSync database. The process of translating the ObjectIds uses a cache (persisted to disk as 'SyncEventTrace-cache.csv') to help speed up the decoding of new ETL traces and reduce the load on the ADSync database. To capture an ETW trace for SyncRulesPipeline debugging use 'Start-ADSyncToolsLogmanTrace' .EXAMPLE Resolve-ADSyncToolsLogmanTrace -Path '.\SyncEventTrace.etl' #> Function Resolve-ADSyncToolsLogmanTrace { [CmdletBinding()] [Alias()] Param ( # Path to ETL trace file [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [string] $Path ) IsAADConnectPresent -MinVersion '1.6' Import-ADSyncToolsModule -ModuleName ADSync -InstallMessage $adSyncInstallMsg Convert-ADSyncToolsLogmanTrace -Path $Path [string] $sourceFilename = $Path + "-converted.csv" [string] $targetFilename = $Path + "-translated.csv" Write-Verbose "sourceFilename = $sourceFilename | targetFilename = $targetFilename" # Init Cache $CacheTable = @{} [string] $cacheFilename = (Split-Path $Path) + "\SyncEventTrace-cache.csv" Initialize-ADSyncToolsLogmanTraceCache -CacheTable ([ref] $CacheTable) -CacheFilename $cacheFilename # Process Sync Event Trace Write-Host "Reading Sync Event Trace file into memory. Please wait..." -ForegroundColor Cyan $syncEventTrace = Get-Content $sourceFilename # Validate Sync Event Trace Header [int] $UserDataCol = 19 $syncEventTraceHeader = $($syncEventTrace[0] -split ',')[$UserDataCol].Trim() If ($syncEventTraceHeader -ne "User Data") { Throw "Unexpected Sync Event Trace header. Error Details: 'User Data' not found in column #$UserDataCol." } # Prepare copy of User Data to ArrayList $totalLines = $syncEventTrace.count Write-Verbose "Copy data to array list ($totalLines entries)" $processTime = [System.Diagnostics.Stopwatch]::StartNew() $syncEventTraceArray = [System.Collections.ArrayList]@() $i = 0 # Init Progress Bar $PercentComplete = 0 $showProgressTimer = [System.Diagnostics.Stopwatch]::StartNew() $msgProgressBar = "Reading Sync Event Trace file" Write-Progress -Activity $msgProgressBar -Status "$PercentComplete,0% Complete:" -PercentComplete $PercentComplete # Copy User Data to ArrayList ForEach ($line in $syncEventTrace) { # Show progress every 3s If ($showProgressTimer.Elapsed.TotalMilliseconds -ge 3000) { $PercentComplete = [math]::round((($i / $totalLines) * 100),1) Write-Progress -Activity $msgProgressBar -Status "$PercentComplete,0% Complete:" -PercentComplete $PercentComplete $showProgressTimer.Reset() $showProgressTimer.Start() } If (-not $line.StartsWith(' EventTrace, ')) # Skip event trace Headers { # Get "User Data" in Sync Event Trace line, don't split commas between double-quotes $l = ($line -split ',+(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)')[$UserDataCol].Trim() $syncEventTraceArray.Add($l) | Out-Null } $i++ } $showProgressTimer.Stop() $processTime.Stop() "Time taken: $($processTime.Elapsed)" # Free-up memory $syncEventTrace = $null # Resolve GUIDs to object names $totalLines = $syncEventTraceArray.count $processTime = [System.Diagnostics.Stopwatch]::StartNew() If ($totalLines -gt 0) { Write-Host "Parsing Sync Event Trace ($totalLines events). Please wait..." -ForegroundColor Cyan $i = 0 # Init Progress Bar $PercentComplete = 0 $showProgressTimer = [System.Diagnostics.Stopwatch]::StartNew() $msgProgressBar = "Resolving object names" Write-Progress -Activity $msgProgressBar -Status "$([math]::round($PercentComplete,1))% Complete:" -PercentComplete $PercentComplete For ($i = 0; $i -lt $totalLines; $i++) { # Show progress every 3s If ($showProgressTimer.Elapsed.TotalMilliseconds -ge 3000) { $PercentComplete = [math]::round((($i / $totalLines) * 100),1) Write-Progress -Activity $msgProgressBar -Status "$PercentComplete% Complete:" -PercentComplete $PercentComplete $showProgressTimer.Reset() $showProgressTimer.Start() } # Parse all GUIDs from line $guidMatches = $syncEventTraceArray[$i] | Select-String -Pattern $guidRegex -AllMatches | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty Value -Unique # Replace each GUID with object name ForEach ($m in $guidMatches) { $objName = Resolve-ADSyncToolsObjectName -CacheTable ([ref] $CacheTable) -Source ConnectorSpace -SourceObjectId $m $syncEventTraceArray[$i] = $syncEventTraceArray[$i].Replace($m, $objName) } } $showProgressTimer.Stop() } $processTime.Stop() "Time taken: $($processTime.Elapsed)" # Save translated trace to disk Write-Verbose "Saving translated Sync Event Trace '$targetFilename' ($totalLines events)..." Try { $syncEventTraceArray | Set-Content $targetFilename } Catch { Throw "There was an error exporting CSV file. Error Details: $($_.Exception.Message)" } # Save updated cache to disk Save-ADSyncToolsLogmanTraceCache -CacheTable ([ref] $CacheTable) -CacheFilename $cacheFilename } #endregion #======================================================================================= #======================================================================================= #region Custom Sync Scheduler #======================================================================================= Function IsSyncCycleRunning { [CmdletBinding()] Param() IsAADConnectPresent $runStatus = Get-ADSyncConnectorRunStatus If ($runStatus.RunState -eq 'Busy') { Return $true } Else { Return $false } } Function IsWizardRunning { [CmdletBinding()] Param() $wizardProc = @(Get-Process | Where {$_.ProcessName -eq 'AzureADConnect'}) If ($wizardProc.Count -gt 0) { Return $true } Else { Return $false } } Function StartRunProfile { [CmdletBinding()] Param( # Connector Name [Parameter(Mandatory=$true, Position=0)] $ConnectorName, # Run Profile Name [Parameter(Mandatory=$true, Position=1)] $RunProfile ) IsAADConnectPresent Write-Output "$(Get-Date) - Running '$RunProfile' step for connector '$ConnectorName'..." Invoke-ADSyncRunProfile -ConnectorName $ConnectorName -RunProfileName $RunProfile | Select ConnectorName, RunProfileName, IsRunComplete, Result | ft } Function ConfirmCustomSyncScheduler { [CmdletBinding()] Param() Write-Verbose "Checking AADConnect Wizard..." If (IsWizardRunning) { # Interrupt Custom sync scheduler Write-Host "`n$(Get-Date) - Azure AD Connect Wizard is running. Custom Sync Scheduler stopped gracefully." -ForegroundColor Cyan Return $false } Write-Verbose "Checking Sync Cycle progress..." If (IsSyncCycleRunning) { # Sync Cycle is currently running, cannot start Throw "`nSync Cycle is in progress. Cannot run Custom Sync Scheduler." } Write-Verbose "Checking Scheduler status..." $syncScheduler = Get-ADSyncScheduler If ($syncScheduler.SyncCycleEnabled) { # Disable Sync Scheduler Try { Set-ADSyncScheduler -SyncCycleEnabled $false } Catch { Throw "Set-ADSyncScheduler failure: $($_.Exception.InnerException)" } Write-Verbose "Sync Scheduler disabled." } Write-Verbose "Checking Maintenance task status..." If (-not $syncScheduler.MaintenanceEnabled) { Try { Set-ADSyncScheduler -MaintenanceEnabled $true } Catch { Throw "Set-ADSyncScheduler failure: $($_.Exception.InnerException)" } Write-Verbose "Maintenance task enabled." } Return $syncScheduler } <# .Synopsis Custom Sync Scheduler to run every sync cycle with a given Connector's order .DESCRIPTION The specific Connector order which is run on a sync cycle (Run Profile) is normally not important but in some scenarios, it can cause sync issues, however, Azure AD Connect cannot guarantee to always run a sync cycle with a specific Connector order. This script DISABLES the built-in Sync Scheduler and provides a synchronous sync scheduler (while the script is running) to honor a given Connector order in every sync cycle. NOTE: This script will not disable the built-in Sync Scheduler in case a sync cycle is already running. Run as a Windows Task Scheduler In case you want to run the Custom Sync Scheduler whether a user is logged on or not, open the Windows Task Scheduler and follow these steps: 1. Create a local folder to store your Custom Sync Scheduler files, e.g.: C:\CustomScheduler\ 2. Create a text file with your specific connector's order, e.g. MyConnectorsOrder.txt: Get-ADSyncConnector | select -ExpandProperty Name | Out-File C:\CustomScheduler\MyConnectorsOrder.txt NOTE: The line above creates a text file which you can edit to set a specific Connector's order 3. Open Windows Task Scheduler: Click Create Task... (not the basic task) 4. In General tab: Name: AADConnect Custom Sync Scheduler Select "Run whether user is logged on or not" Enable "Do not store password" Set "Configure for:" with your current operating system version, e.g.: Windows Server 2019 5. In Triggers tab: Click New... Daily - Recur every '1' days Enable "Repeat task every '30 minutes'" 6. In Actions tab: Click New... Program/script: powershell Add arguments: -command &{Import-Module "C:\Program Files\Microsoft Azure Active Directory Connect\Tools\AdSyncTools.psm1"; Start-ADSyncToolsCustomSyncScheduler -ConnectorsOrderFilename "C:\CustomScheduler\MyConnectorsOrder.txt" -RunProfile Delta >>"C:\CustomScheduler\ADSyncCustomSyncScheduler.log"} 7. In Settings tab: Disable "Stop the task if it runs longer than" 8. Run the new task and check in the Synchronization Service Manager if a new delta sync cycle has started. .EXAMPLE $myConnectorsOrder = @('Contoso.com','Contoso.onmicrosoft.com - AAD') NOTE: The line above creates a list (array) with your specific Connector's order, then start the Custom Sync Scheduler with: Start-ADSyncToolsCustomSyncScheduler -ConnectorsOrderList $myConnectorsOrder .EXAMPLE Get-ADSyncConnector | select -ExpandProperty Name | Out-File .\MyConnectorsOrder.txt NOTE: The line above creates a text file which you can edit to set a specific Connector's order, then start the Custom Sync Scheduler with: Start-ADSyncToolsCustomSyncScheduler -ConnectorsOrderFilename .\MyConnectorsOrder.txt .EXAMPLE Start-ADSyncToolsCustomSyncScheduler -ConnectorsOrderList @('Contoso.com','Contoso.onmicrosoft.com - AAD') -RunProfile Delta NOTE: This will run a single Delta sync cycle, without starting the custom sync scheduler. .EXAMPLE Start-ADSyncToolsCustomSyncScheduler -ConnectorsOrderFilename .\MyConnectorsOrder.txt -RunProfile Full NOTE: This will run a single Full sync cycle, without starting the custom sync scheduler. #> Function Start-ADSyncToolsCustomSyncScheduler { [CmdletBinding()] Param ( # Custom Sync Scheduler Connectors order as a collection [Parameter(ParameterSetName='Array', Mandatory=$true, Position=0)] [string[]] $ConnectorsOrderList, # Custom Sync Scheduler Connectors order [Parameter(ParameterSetName='Filename', Mandatory=$true, Position=0)] [string] $ConnectorsOrderFilename, # Run Profile (Use "Scheduler" to enable custom sync scheduler or "Full"/"Delta" for a single sync cycle [Parameter(Mandatory=$false, Position=1)] [ValidateSet("Scheduler", "Full", "Delta", IgnoreCase = $false)] [string] $RunProfile = "Scheduler" ) $ErrorActionPreference = 'Stop' switch ($PSCmdlet.ParameterSetName) { 'Array' { $ADSyncConnectorsOrder = $ConnectorsOrderList } 'Filename' { Try { $ADSyncConnectorsOrder = @(Get-Content $ConnectorsOrderFilename) } Catch { Throw "Error reading the file '$ConnectorsOrderFilename'. Error Details: $($_.Exception.Message)" } } } Write-Host "`nCustom Sync Scheduler Connectors order:" -ForegroundColor Green $ADSyncConnectorsOrder Write-Verbose "Connectors Count = $(@($ADSyncConnectorsOrder).Count)" If (@($ADSyncConnectorsOrder).Count -lt 2) { Throw "Invalid Connector order. Please use 'get-help ADSyncToolsCustomSyncScheduler.ps1 -Full' for more information." } if ($RunProfile -eq 'Scheduler') { $runProfileName = 'Delta' $customSyncSchedulerEnabled = $true } Else { $runProfileName = $RunProfile $customSyncSchedulerEnabled = $false } Write-Verbose "CustomSyncSchedulerEnabled = $customSyncSchedulerEnabled" Write-Verbose "RunProfileName = $runProfileName" $checkDelaySeconds = 0 $customSyncCycleNextStartUTC = Get-Date 0 # Enter Sync Scheduler :MainLoop Do { Do { Write-Verbose "Confirm Custom Sync Scheduler" $syncSchedulerSettings = ConfirmCustomSyncScheduler if ($syncSchedulerSettings) { $syncCycleIntervalMins = $syncSchedulerSettings.CurrentlyEffectiveSyncCycleInterval.Minutes $currentTimeUTC = (Get-Date).ToUniversalTime() Write-Verbose "Sleeping for $checkDelaySeconds seconds..." Start-Sleep -Seconds $checkDelaySeconds } Else { # Wizard open, exit Custom Sync Scheduler Break MainLoop } } While ($currentTimeUTC -lt $customSyncCycleNextStartUTC) # Calculate next sync cycle start time $customSyncCycleStartUTC = (Get-Date).ToUniversalTime() $customSyncCycleNextStartUTC = $customSyncCycleStartUTC + $(New-TimeSpan -Minutes $syncCycleIntervalMins) $checkDelaySeconds = 10 # Start a new Sync Cycle Write-Host "`n$(Get-Date) - Sync Cycle Start" -ForegroundColor Green Write-Host 'Sync Cycle started - Please do not interrupt sync cycles.' -ForegroundColor Yellow # Import Step ForEach ($c in $ADSyncConnectorsOrder) { $runProfileFullName = $runProfileName + ' Import' StartRunProfile -ConnectorName $c -RunProfile $runProfileFullName } # Synchronization Step ForEach ($c in $ADSyncConnectorsOrder) { $runProfileFullName = $runProfileName + ' Synchronization' StartRunProfile -ConnectorName $c -RunProfile $runProfileFullName } # Export Step If (-not $syncSchedulerSettings.StagingModeEnabled) { ForEach ($c in $ADSyncConnectorsOrder) { $runProfileFullName = 'Export' StartRunProfile -ConnectorName $c -RunProfile $runProfileFullName } } If ($customSyncSchedulerEnabled) { # Show wait time for next sync cycle $customSyncCycleFinishUTC = (Get-Date).ToUniversalTime() $customSyncCycleWaitTimeSeconds = [math]::Round(($customSyncCycleNextStartUTC - $customSyncCycleFinishUTC).TotalSeconds) If ($customSyncCycleWaitTimeSeconds -lt 0) { $customSyncCycleWaitTimeSeconds = 0 } Write-Host "$(Get-Date) - Next Sync Cycle Starting in $customSyncCycleWaitTimeSeconds seconds..." -ForegroundColor Green Write-Host 'To stop Custom Sync Scheduler, please launch Azure AD Connect Wizard.' -ForegroundColor Yellow } } While ($customSyncSchedulerEnabled) } #endregion #======================================================================================= #======================================================================================= #region Active Directory Permissions Troubleshooting #======================================================================================= # Set/Create OutputDirectory global variable Function Set-OutputDirectory { [CmdletBinding()] Param() # ADsync Diags output sub-folder $outputFolder = "ADSyncTools-Output" [string] $currentLocation = (Get-Location).Path If (-not [string]::IsNullOrEmpty($currentLocation)) { [string] $global:outputPath = "$currentLocation\$outputFolder" # Create Output Folder If (-not (Test-Path $global:outputPath)) { Try { $newfolder = New-Item -Path $global:outputPath -ItemType directory } Catch { Throw "Unable to set output folder. Error Details: $($_.Exception.Message)" } } Write-Verbose "Current output folder: $($global:outputPath)" } Else { Throw "Unable to get working folder." } } # Initialtes the HTML report headers and filename Function Initialize-ADSyncToolsHtmlReport { [CmdletBinding()] Param() If ($PSVersionTable.PSVersion.Major -gt 2) { $reportStyleHtml = @" /* ADSyncTools-Output.css file to format HMTL report */ p{ line-height: 1em; } h1, h2, h3, h4{ color: DodgerBlue; font-weight: normal; line-height: 1.1em; margin: 0 0 .5em 0; } h1{ font-size: 1.7em; } h2{ font-size: 1.5em; } a{ color: black; text-decoration: none; } a:hover, a:active{ text-decoration: underline; } body{ font-family: arial; font-size: 80%; line-height: 1.2em; width: 100%; margin: 0; background: white; } "@ # Create the Cascading Style Sheet (CSS) file $outputReportStyleFile = "ADSyncTools-Output.css" Try { $reportStyleHtml | Out-File -FilePath "$global:outputPath\$outputReportStyleFile" } Catch { Write-Error "Error creating '$global:ADSyncToolsOutputStyle' file in '$global:outputPath'. Error Details: $($_.Exception.Message)" } # Init the HTML elements in memory - Title and date $Global:ADSyncToolsHtmlReport = $null [string] $reportLongDate = "$((Get-Date).ToUniversalTime().DateTime) UTC" [string] $reportTitle = 'AAD Connect Diagnostics' $htmlReport = ConvertTo-Html -CssUri $outputReportStyleFile -Body "<H1>$reportTitle</H1><p>$reportLongDate</p>" -Title $reportTitle $i=0 while ($htmlReport[$i] -ne "<table>") { $Global:ADSyncToolsHtmlReport += "$($htmlReport[$i])`n" $i++ } $Global:ADSyncToolsHtmlReport += "<p></p>`n" } else { $Global:ADSyncToolsHtmlReport = $null } } # Adds the input content into HTML fragments to the report Function Export-ADSyncToolsHtmReport { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $Title, [Parameter(Mandatory=$false)] $InputObject, [ValidateSet("List","Table","String")] $As="Table" ) If ($Global:ADSyncToolsHtmlReport -ne $null) { If ($As -eq "String") { $Global:ADSyncToolsHtmlReport += "<p>$Title$InputObject<p>`n" } Else { $Global:ADSyncToolsHtmlReport += "<H2>$Title</H2>`n" $Global:ADSyncToolsHtmlReport += $InputObject | ConvertTo-Html -Fragment -As $As $Global:ADSyncToolsHtmlReport += "<p></p>`n" } } } # Finalizes the report and saves the HTML file Function Close-ADSyncToolsHtmlReport { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $Title, [Parameter(Mandatory=$true)] $ReportDate ) If ($Global:ADSyncToolsHtmlReport -ne $null) { $filename = "$global:outputPath\$($ReportDate)_ADSyncTools_$Title.htm" $Global:ADSyncToolsHtmlReport += "</body></html>" Try { $Global:ADSyncToolsHtmlReport | Out-File -FilePath $filename Write-Host "Exported HTML Report to file $filename" } Catch { Write-Error "An error occurred exporting HTML Report to '$filename'. Error Details: $($_.Exception.Message)" } } } # Exports report data into a standalone XML file Function Export-ADSyncToolsXmlReport { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $Title, [Parameter(Mandatory=$true)] $InputObject, [Parameter(Mandatory=$true)] $ReportDate ) $filename = "$global:outputPath\$($ReportDate)_ADSyncTools_$Title.xml" Try { # Export data to XML file $InputObject | Export-Clixml $filename } Catch { Write-Error "An error occurred exporting data to file '$filename'. Error Details: $($_.Exception.Message)" } } <# .Synopsis Find an Active Directory object in the Forest by its DOMAIN\username'. .DESCRIPTION Supports multi-domain queries and returns all the required properties including mS-DS-ConsistencyGuid. TODO - Replace by Search-ADSyncToolsADobject #> Function Get-ADSyncToolsADobjectByDomainUsername { [CmdletBinding()] Param ( [Parameter(Mandatory=$true, Position=0)] [string] $DomainUsername ) Write-Verbose "Enter: Get-ADSyncToolsADobjectByDomainUsername -DomainUsername $DomainUsername" Write-Verbose "'$DomainUsername' -match regex Domain: $($DomainUsername -match $netbiosDomainRegex)" Write-Verbose "'$DomainUsername' -match regex UPN: $($DomainUsername -match $upnRegex)" If ($DomainUsername -match $netbiosDomainRegex) { # DOMAIN\USER input format $domainUsernameA = $DomainUsername -split '\\' } ElseIf ($DomainUsername -match $upnRegex) { # UPN input format $DomainUsernameA = $DomainUsername -split '@' [array]::Reverse($DomainUsernameA) } Else { Throw "Invalid input. Make sure you are using a valid domain account in 'DOMAIN\username' or UPN format." } <# TODO - suport for multi-domain query Try { $userDomainObj = Get-ADDomain $domainAccount[0] -ErrorAction Stop Write-Verbose "Found Domain $userDomainObj" $userDomain = $userDomainObj.DistinguishedName } Catch { Write-Error "Unable to find Domain $($domainAccount[0]) : $($_.Exception.Message)" return $null } #> # Get the AD object from target DC Write-Verbose "Executing: Get-ADObject -Filter `"sAMAccountName -eq '$($domainUsernameA[1])'`" -Properties $defaultADobjProperties -SearchBase $domainDN -SearchScope Subtree -Server $targetDC" Try { $seachResult = Get-ADObject -Filter "sAMAccountName -eq '$($domainUsernameA[1])'" -Properties $defaultADobjProperties -ErrorAction Stop #TODO: -SearchBase $domainDN -SearchScope Subtree -Server $targetDC } Catch { Throw "Cannot find user '$DomainUsername': $($_.Exception.Message)" } Write-Verbose "Exit: Get-ADSyncToolsADobjectByDomainUsername" Return $seachResult } # Convert SIDs to readable names Function Convert-SIDtoName { [CmdletBinding()] Param ( $sid ) # Super Verbose # Write-Verbose $sid Try { $ID = New-Object System.Security.Principal.SecurityIdentifier($sid) $User = $ID.Translate( [System.Security.Principal.NTAccount]) $User.Value } Catch { Switch($sid) { #Reference http://support.microsoft.com/kb/243330 "S-1-0" { "Null Authority" } "S-1-0-0" { "Nobody" } "S-1-1" {"World Authority" } "S-1-1-0" { "Everyone" } "S-1-2" { "Local Authority" } "S-1-2-0" { "Local" } "S-1-2-1" { "Console Logon" } "S-1-3" { "Creator Authority" } "S-1-3-0" { "Creator Owner" } "S-1-3-1" { "Creator Group" } "S-1-3-4" { "Owner Rights" } "S-1-5-80-0" {"All Services" } "S-1-4" { "Non Unique Authority" } "S-1-5" { "NT Authority" } "S-1-5-1" { "Dialup" } "S-1-5-2" { "Network" } "S-1-5-3" { "Batch" } "S-1-5-4" { "Interactive" } "S-1-5-6" { "Service" } "S-1-5-7" { "Anonymous" } "S-1-5-9" { "Enterprise Domain Controllers"} "S-1-5-10" { "Self" } "S-1-5-11" { "Authenticated Users" } "S-1-5-12" { "Restricted Code" } "S-1-5-13" { "Terminal Server Users" } "S-1-5-14" { "Remote Interactive Logon" } "S-1-5-15" { "This Organization" } "S-1-5-17" { "This Organization" } "S-1-5-18" { "Local System" } "S-1-5-19" { "NT Authority Local Service" } "S-1-5-20" { "NT Authority Network Service" } "S-1-5-32-544" { "Administrators" } "S-1-5-32-545" { "Users"} "S-1-5-32-546" { "Guests" } "S-1-5-32-547" { "Power Users" } "S-1-5-32-548" { "Account Operators" } "S-1-5-32-549" { "Server Operators" } "S-1-5-32-550" { "Print Operators" } "S-1-5-32-551" { "Backup Operators" } "S-1-5-32-552" { "Replicators" } "S-1-5-32-554" { "Pre-Windows 2000 Compatibility Access"} "S-1-5-32-555" { "Remote Desktop Users"} "S-1-5-32-556" { "Network Configuration Operators"} "S-1-5-32-557" { "Incoming forest trust builders"} "S-1-5-32-558" { "Performance Monitor Users"} "S-1-5-32-559" { "Performance Log Users" } "S-1-5-32-560" { "Windows Authorization Access Group"} "S-1-5-32-561" { "Terminal Server License Servers"} "S-1-5-32-561" { "Distributed COM Users"} "S-1-5-32-569" { "Cryptographic Operators" } "S-1-5-32-573" { "Event Log Readers" } "S-1-5-32-574" { "Certificate Services DCOM Access" } "S-1-5-32-575" { "RDS Remote Access Servers" } "S-1-5-32-576" { "RDS Endpoint Servers" } "S-1-5-32-577" { "RDS Management Servers" } "S-1-5-32-575" { "Hyper-V Administrators" } "S-1-5-32-579" { "Access Control Assistance Operators" } "S-1-5-32-580" { "Remote Management Users" } default {$sid} } } } # Convert schema GUID's to readable names Function Convert-GUIDtoName { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [string] $guid, [switch] $extended ) $guidval = [Guid]$guid $bytearr = $guidval.tobytearray() $bytestr = "" ForEach ($byte in $bytearr) { $str = "\" + "{0:x}" -f $byte $bytestr += $str } If ($extended) { #for extended rights, we can check in the configuration container $de = New-Object directoryservices.directoryentry("LDAP://" + ([adsi]"LDAP://rootdse").psbase.properties.configurationnamingcontext) $ds = New-Object directoryservices.directorysearcher($de) $ds.propertiestoload.add("displayname") | Out-Null $ds.filter = "(rightsguid=$guid)" $result = $ds.findone() } Else { #Search schema for possible matches for this GUID $de = New-Object directoryservices.directoryentry("LDAP://" + ([adsi]"LDAP://rootdse").psbase.properties.schemanamingcontext) $ds = New-Object directoryservices.directorysearcher($de) $ds.filter = "(|(schemaidguid=$bytestr)(attributesecurityguid=$bytestr))" $ds.propertiestoload.add("ldapdisplayname") | Out-Null $result = $ds.findone() } If ($result -eq $null) { If ($guid -like '00000000-0000-0000-0000-000000000000') { Return "" } Else { Return $guid } } Else { If ($extended) { $result.properties.displayname } Else { $result.properties.ldapdisplayname } } } # Parse Active Directory Access Rights and translate extended access rights Function Translate-ADSyncToolsExtendedAccessRights { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $objectDACL ) $accessRightsList = @() ForEach ($ace in $objectDACL) { $accessRights = New-Object PSobject -Property @{ ActiveDirectoryRights = $ace.ActiveDirectoryRights InheritanceType = $ace.InheritanceType ObjectType = "" InheritedObjectType = Convert-GUIDtoName -guid $($ace.inheritedobjecttype) ObjectFlags = $ace.ObjectFlags AccessControlType = $ace.accesscontroltype IdentityReference = Convert-SIDtoName -sid $($ace.identityReference) IsInherited = $ace.isinherited InheritanceFlags = $ace.InheritanceFlags PropagationFlags = $ace.PropagationFlags } If ($ace.ActiveDirectoryRights -eq "ExtendedRight") { $accessRights.ObjectType = Convert-GUIDtoName -guid $($ace.objecttype) -extended } Else { $accessRights.ObjectType = Convert-GUIDtoName -guid $($ace.objecttype) } $accessRightsList += $accessRights } Return $($accessRightsList | select ActiveDirectoryRights,AccessControlType,IdentityReference,ObjectType,InheritedObjectType,ObjectFlags,IsInherited,InheritanceType,InheritanceFlags,PropagationFlags) } # Function used by Get-ADSyncToolsUsrMemberOfTransitive to get Group membership recursively. Function Get-ADSyncToolsGrpMemberOfRecursive { [CmdletBinding()] Param ( $ADobject ) $groups = Get-ADPrincipalGroupMembership -Identity $($ADobject.distinguishedName) foreach ($g in $groups) { # Call recursive function Get-ADSyncToolsGrpMemberOfRecursive ($g) # Return the group Object Get-ADObject $($g.distinguishedName) -Properties CanonicalName,msDS-PrincipalName | select CanonicalName,DistinguishedName,msDS-PrincipalName,Name,ObjectClass,ObjectGUID } } # Receives a user account as input (AD object) # Returns the group membership including Nested-Groups, Foreign-Security-Principals and its own identity reference Function Get-ADSyncToolsUsrMemberOfTransitive { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $UserAccount ) # Get AD DS Connector Account group membership from AD including PrimaryGroup Try { $srvAccountMemberOf = @(Get-ADPrincipalGroupMembership $($UserAccount.distinguishedName) -ErrorAction Stop) } Catch { Write-Error "Unable to run Get-ADPrincipalGroupMembership for target object: $($_.Exception.Message)" } # Add all Groups to an Array of Group Objects $srvAccountMemberOfObj = @() ForEach ($group in $srvAccountMemberOf) { $srvAccountMemberOfObj += Get-ADObject $($group.distinguishedName) -Properties CanonicalName,msDS-PrincipalName | select CanonicalName,DistinguishedName,msDS-PrincipalName,Name,ObjectClass,ObjectGUID } # Add Authenticated Users Group into the Array of Group Objects $authUsersGroup = Get-ADObject -Filter {ObjectClass -eq "foreignSecurityPrincipal"} -Properties CanonicalName,msDS-PrincipalName | Where-Object {$_.'msDS-PrincipalName' -like "*Authenticated Users*"} | select CanonicalName,DistinguishedName,msDS-PrincipalName,Name,ObjectClass,ObjectGUID $srvAccountMemberOfObj += $authUsersGroup # Get Authenticated Users nested groups $authUsersMemberOf = (Get-ADobject $($authUsersGroup.distinguishedName) -Properties memberOf).memberOf # Add Authenticated Users nested groups into the Array of Group Objects foreach ($group in $authUsersMemberOf) { $srvAccountMemberOfObj += Get-ADObject $group -Properties CanonicalName,msDS-PrincipalName | select CanonicalName,DistinguishedName,msDS-PrincipalName,Name,ObjectClass,ObjectGUID } # Get all nested groups into an array of Group Objects $srvAccountNestedMemberOfObj = @() foreach ($group in $srvAccountMemberOfObj) { if ($group.ObjectClass -eq 'group') { #$group.CanonicalName $srvAccountNestedMemberOfObj += @(Get-ADSyncToolsGrpMemberOfRecursive ($group)) } } # Add all nested groups into the main Array of Group Objects $srvAccountMemberOfObj += $srvAccountNestedMemberOfObj # Add Everyone Group to the list of Identities $everyoneGroup = "" | select CanonicalName,DistinguishedName,msDS-PrincipalName,Name,ObjectClass,ObjectGUID $everyoneGroup.CanonicalName = "Everyone" $everyoneGroup.DistinguishedName = "S-1-1-0" $everyoneGroup.'msDS-PrincipalName' = "Everyone" $everyoneGroup.Name = "Everyone" $everyoneGroup.ObjectClass = "well-known-sid" $srvAccountMemberOfObj += $everyoneGroup Write-Verbose "" Write-Verbose "--- AD DS Connector Account full group membership ---`n$($srvAccountMemberOfObj.'msDS-PrincipalName' | sort -Unique | %{"`n$_"}) `n" # Add the account itself to the list of Identities #$adObject = Invoke-Command -Session $ADSyncToolsPsSession { Get-ADObject $args[0] -Properties CanonicalName,msDS-PrincipalName } -Args $UserAccount.DistinguishedName | select CanonicalName,DistinguishedName,msDS-PrincipalName,Name,ObjectClass,ObjectGUID $adObject = Get-ADObject $($UserAccount.DistinguishedName) -Properties CanonicalName,msDS-PrincipalName | select CanonicalName,DistinguishedName,msDS-PrincipalName,Name,ObjectClass,ObjectGUID $srvAccountMemberOfObj += $adObject Write-Verbose "" Write-Verbose "--- AD DS Connector Account --- `n`n$($adObject.'msDS-PrincipalName') `n" # Remove duplicates $srvAccountMemberOfObj = $srvAccountMemberOfObj | sort DistinguishedName -Unique Return $srvAccountMemberOfObj } # Exports permissions to HTML and XML files Function Export-ADSyncToolsADpermissions { [CmdletBinding()] Param ( [Parameter(Mandatory=$true, Position=0)] $ADtarget, [Parameter(Mandatory=$true, Position=1)] $ADroot, [Parameter(Mandatory=$true, Position=2)] $ADconnectorAccount, [Parameter(Mandatory=$true, Position=3)] $ADSyncSrvAccount, [Parameter(Mandatory=$true, Position=4)] $ADbuiltinContainer, [Parameter(Mandatory=$true, Position=5)] $ADsamServer ) Write-Verbose "Enter: Export-ADSyncToolsADpermissions" # Init report Set-OutputDirectory Initialize-ADSyncToolsHtmlReport $reportDate = [string] $((Get-Date).toString('yyyyMMdd-HHmmss')) # AD DS Connector object $adConnectorDetails = $ADconnectorAccount | select $defaultADobjProperties Export-ADSyncToolsHtmReport -InputObject $adConnectorDetails -Title "AD DS Connector Account '$($adConnectorDetails.CanonicalName)' details:" -As List # ADSync Service Account object $adSyncSrvDetails = $ADSyncSrvAccount #| select $defaultADobjProperties Export-ADSyncToolsHtmReport -InputObject $adSyncSrvDetails -Title "ADSync Service Account '$($adSyncSrvDetails.CanonicalName)' details:" -As List # Target AD object $ADtargetDetails = $ADtarget | select $defaultADobjProperties Export-ADSyncToolsHtmReport -InputObject $ADtargetDetails -Title "Target AD object '$($ADtarget.CanonicalName)' details:" -As List Export-ADSyncToolsXmlReport -InputObject $ADtargetDetails -Title "ADtarget-Details" -ReportDate $reportDate # AD Domain Root Container $adRootDetails = $ADroot | select $defaultADobjProperties Export-ADSyncToolsHtmReport -InputObject $adRootDetails -Title "AD Root container '$($ADroot.CanonicalName)' details:" -As List Export-ADSyncToolsXmlReport -InputObject $adRootDetails -Title "DomainRoot-Details" -ReportDate $reportDate # AD Builtin Container $adBuiltinDetails = $ADbuiltinContainer | select $defaultADobjProperties Export-ADSyncToolsHtmReport -InputObject $adBuiltinDetails -Title "AD Builtin container '$($ADbuiltinContainer.CanonicalName)' details:" -As List Export-ADSyncToolsXmlReport -InputObject $adBuiltinDetails -Title "Builtin-Details" -ReportDate $reportDate # AD SAM Server object $adSamServerDetails = $ADsamServer | select $defaultADobjProperties Export-ADSyncToolsHtmReport -InputObject $adSamServerDetails -Title "AD SAM Server object '$($ADsamServer.CanonicalName)' details:" -As List Export-ADSyncToolsXmlReport -InputObject $adSamServerDetails -Title "SamServer-Details" -ReportDate $reportDate # Get Full ACL of target AD object, Root AD Container, Builtin container and SAM Server $adTargetACL = Get-ADSyncToolsADpermissions -ADobject $ADtarget $adRootACL = Get-ADSyncToolsADpermissions -ADobject $ADroot $adBuiltinACL = Get-ADSyncToolsADpermissions -ADobject $ADbuiltinContainer $adSamServerACL = Get-ADSyncToolsADpermissions -ADobject $ADsamServer # Get AD DS Connector Account full group membership $adConnectorGroups = @(Get-ADSyncToolsUsrMemberOfTransitive -UserAccount $ADconnectorAccount) If ($adConnectorGroups.Count -gt 0) { # Export AD DS Connector Account Groups Export-ADSyncToolsHtmReport -InputObject $adConnectorGroups -Title "AD DS Connector Account '$($adConnectorDetails.CanonicalName)' group membership:" -As Table Export-ADSyncToolsXmlReport -InputObject $adConnectorGroups -Title "ADConnectorAccGroups" -ReportDate $reportDate # Calculate Effective Permissions of ADconnectorAccount over the ADtarget $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adTargetACL -ADconnectorAccGroups $adConnectorGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "AD DS Connector Account effective permissions over target AD object" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADConnectorOnADtarget-EffectiveDACL" -ReportDate $reportDate # Calculate Effective Permissions of ADconnectorAccount over the Domain Root $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adRootACL -ADconnectorAccGroups $adConnectorGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "AD DS Connector Account effective permissions over Domain Root" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADConnectorOnDomainRoot-EffectiveDACL" -ReportDate $reportDate # Calculate Effective Permissions of ADconnectorAccount over the Builtin Container $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adBuiltinACL -ADconnectorAccGroups $adConnectorGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "AD DS Connector Account effective permissions over Builtin Container" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADConnectorOnBuiltin-EffectiveDACL" -ReportDate $reportDate # Calculate Effective Permissions of ADconnectorAccount over the SAM Server object $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adSamServerACL -ADconnectorAccGroups $adConnectorGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "AD DS Connector Account effective permissions over SAM Server object" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADConnectorOnSamServer-EffectiveDACL" -ReportDate $reportDate } Else { Write-Warning "Unable to calculate effective permissions for AD DS Connector Account." } <# TODO support for all types of ADSync service accounts (domain, MSA, VSA, gMSA) # Get ADSync Service Account full group membership $adSyncSrvGroups = @(Get-ADSyncToolsUsrMemberOfTransitive -UserAccount $ADSyncSrvAccount) If ($adSyncSrvGroups.Count -gt 0) { # Export ADSync Service Account Groups Export-ADSyncToolsHtmReport -InputObject $adSyncSrvGroups -Title "ADSync Service Account '$($adSyncSrvDetails.CanonicalName)' group membership:" -As Table Export-ADSyncToolsXmlReport -InputObject $adSyncSrvGroups -Title "ADSyncAccGroups" -ReportDate $reportDate # Calculate Effective Permissions of ADSync Service Account over the ADtarget $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adTargetACL -ADconnectorAccGroups $adSyncSrvGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "ADSync Service Account effective permissions over target AD object" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADSyncOnADtarget-EffectiveDACL" -ReportDate $reportDate # Calculate Effective Permissions of ADSync Service Account over the Domain Root $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adRootACL -ADconnectorAccGroups $adSyncSrvGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "ADSync Service Account effective permissions over Domain Root" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADSyncOnDomainRoot-EffectiveDACL" -ReportDate $reportDate # Calculate Effective Permissions of ADSync Service Account over the Builtin Container $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adBuiltinACL -ADconnectorAccGroups $adSyncSrvGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "ADSync Service Account effective permissions over Builtin Container" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADSyncOnBuiltin-EffectiveDACL" -ReportDate $reportDate # Calculate Effective Permissions of ADSync Service Account over the SAM Server object $effectiveDACL = Get-ADSyncToolsADeffectivePermissions -ADPermissions $adSamServerACL -ADconnectorAccGroups $adSyncSrvGroups # Export effective permissions to a XML and HTML Export-ADSyncToolsHtmReport -InputObject $effectiveDACL -Title "ADSync Service Account effective permissions over SAM Server object" -As Table Export-ADSyncToolsXmlReport -InputObject $effectiveDACL -Title "ADSyncOnSamServer-EffectiveDACL" -ReportDate $reportDate } Else { Write-Warning "Unable to calculate effective permissions for ADSync Service Account." } #> # Export Full ACL of target AD Object Export-ADSyncToolsHtmReport -InputObject $adTargetACL -Title "Full permissions of object '$($ADtarget.CanonicalName)'" -As Table Export-ADSyncToolsXmlReport -InputObject $adTargetACL -Title "ADtarget-FullDACL" -ReportDate $ReportDate # Export Full ACL of Domain Root Container Export-ADSyncToolsHtmReport -InputObject $adRootACL -Title "Full permissions of object '$($ADroot.CanonicalName)'" -As Table Export-ADSyncToolsXmlReport -InputObject $adRootACL -Title "DomainRoot-FullDACL" -ReportDate $ReportDate # Export Full ACL of Builtin Container Export-ADSyncToolsHtmReport -InputObject $adBuiltinACL -Title "Full permissions of object '$($ADbuiltinContainer.CanonicalName)'" -As Table Export-ADSyncToolsXmlReport -InputObject $adBuiltinACL -Title "Builtin-FullDACL" -ReportDate $ReportDate # Export Full ACL of SAM Server object Export-ADSyncToolsHtmReport -InputObject $adSamServerACL -Title "Full permissions of object '$($ADsamServer.CanonicalName)'" -As Table Export-ADSyncToolsXmlReport -InputObject $adSamServerACL -Title "SamServer-FullDACL" -ReportDate $ReportDate # Export file system permissions $fileSystemACL = Get-ADSyncToolsFSpermissions "C:\Windows\System32" Export-ADSyncToolsHtmReport -InputObject $fileSystemACL -Title "Full permssions of 'System32' folder" -As Table Export-ADSyncToolsXmlReport -InputObject $fileSystemACL -Title "System32-FullDACL" -ReportDate $ReportDate $fileSystemACL = Get-ADSyncToolsFSpermissions "C:\Windows\System32\samlib.dll" Export-ADSyncToolsHtmReport -InputObject $fileSystemACL -Title "Full permssions of 'Samlib' file" -As Table Export-ADSyncToolsXmlReport -InputObject $fileSystemACL -Title "Samlib-FullDACL" -ReportDate $ReportDate # Close HTML report Close-ADSyncToolsHtmlReport -Title "_ADpermissionsReport" -ReportDate $reportDate # Export Group Policies Export-ADSyncToolsGroupPolicies -ReportDate $reportDate # Export NTDS service on localhost Export-ADSyncToolsDomainServices -ReportDate $reportDate Write-Verbose "Exit: Export-ADSyncToolsADpermissions" } Function Export-ADSyncToolsGroupPolicies { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $ReportDate ) # Export Group Policies $filename = "$global:outputPath\" + "$($ReportDate)_ADSyncTools__$($env:COMPUTERNAME)-GPresult.htm" gpresult /H $filename } Function Export-ADSyncToolsDomainServices { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $ReportDate ) # Export Domain Controller Service (NTDS) $ntdsSrv = Get-Service NTDS -ErrorAction SilentlyContinue If ([string]::IsNullOrEmpty($ntdsSrv)) { $ntdsSrv = "Active Directory Domain Services not found on localhost." } Write-Verbose $ntdsSrv $filename = "$global:outputPath\" + "$($ReportDate)_ADSyncTools_$($env:COMPUTERNAME)-DomainServices.xml" Export-Clixml -InputObject $ntdsSrv -Path $filename } # Returns the list of permissions (DACL) of a given object (ADobject) Function Get-ADSyncToolsADpermissions { [CmdletBinding()] Param ( $ADobject ) Write-Verbose "Enter: Get-ADSyncToolsADpermissions" # Move to AD PS Drive $currentDrive = Get-Location Set-Location AD: # Get and translate DACL of object in AD Try { $permissionsRaw = (Get-Acl $($ADobject.distinguishedName)).Access } Catch { Throw "A problem occurred reading AD ACLs. Error Details: $($_.Exception.Message)" } Finally { Set-Location $currentDrive } # Translate ActiveDirectory Extended Access Rights Write-Verbose "Translating ActiveDirectory Extended Access Rights from AD object '$($ADobject.distinguishedName)'..." $permissions = Translate-ADSyncToolsExtendedAccessRights $permissionsRaw Write-Verbose "Exit: Get-ADSyncToolsADpermissions" Return $permissions } # Returns the list of permissions (DACL) of a given file system path Function Get-ADSyncToolsFSpermissions { [CmdletBinding()] Param ( $Path ) Write-Verbose "Enter: Get-ADSyncToolsFSpermissions" # Get and translate DACL of object in AD If (Test-Path $Path) { Try { $permissions = (Get-Acl $Path).Access } Catch { Write-Error "A problem occurred reading file system ACLs. Error Details: $($_.Exception.Message)" } } Else { Write-Error "Path '$Path' not found." } Write-Verbose "Exit: Get-ADSyncToolsFSpermissions" Return $permissions } # Returns the list of effective permissions (DACL) of a given object (ADPermissions) based on a list of groups (ADconnectorAccGroups) # Exports All permissions to XML Function Get-ADSyncToolsADeffectivePermissions { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] $ADPermissions, [Parameter(Mandatory=$true)] $ADconnectorAccGroups ) Write-Verbose "Enter: Get-ADSyncToolsADeffectivePermissions" # Build full group membership list in Domain\groupname format $ADconnectorAccGroupList = $adConnectorAccGroups.'msDS-PrincipalName' # Filter ACEs of the AD MA Account object and any Groups that AD MA Account belogs to Write-Verbose "`n--- Group Transitive Membership ---`n" $effectiveDACL = @() foreach ($ace in $ADPermissions) { $aceName = [string] $ace.IdentityReference if ($ADconnectorAccGroupList -contains $aceName) { $effectiveDACL +=$ace Write-Verbose "$aceName" } } Write-Verbose "`n--- Effective AD Permissions DACL ---`n" # Expand ActiveDirectoryRights (CreateChild, Self, WriteProperty, ExtendedRight, Delete, GenericRead, WriteDacl, WriteOwner) into separated ACEs $expEffectiveDACL = @() foreach ($ace in $effectiveDACL) { $adRights = ($ace.ActiveDirectoryRights.ToString()) -split ', ' if ($adRights.Count -gt 1) { foreach ($adRight in $adRights) { $aceCopy = $ace | select * # new clone of the ACE instance $aceCopy.ActiveDirectoryRights = $adRight $expEffectiveDACL += $aceCopy } } else { # no need to expand ActiveDirectoryRights, just casting from Enum to string $ace.ActiveDirectoryRights = [string] $ace.ActiveDirectoryRights $expEffectiveDACL += $ace } } Write-Verbose "$($expEffectiveDACL | select ActiveDirectoryRights,AccessControlType,IdentityReference | Out-String)" Write-Verbose "Exit: Get-ADSyncToolsADeffectivePermissions" Return $expEffectiveDACL } <# .Synopsis Exports AD effective/permissions that AD DS Connector Account has over an object .DESCRIPTION This function takes as input an AD object 'DistinguishedName' (-ADobjectDN) and the AD DS Connector Account 'Domain\username' (ADconnectorAccount) to calculate the effective AD permissions over that AD object. It also retrieves a list of effective permission over the AD root container object from the Domain where that AD object belongs. Outputs to screen and XML/HTML files these effective AD permissions, as well as a full dump of all permissions of the AD object and other related objects. .EXAMPLE Export-ADSyncToolsADpermissionsReport -ADobjectDN "CN=User1,OU=AADconnect,DC=Contoso,DC=com" -ADconnectorAccount "Contoso\ADsyncSvc" .EXAMPLE Export-ADSyncToolsADpermissionsReport -ADobjectDN "CN=User1,OU=AADconnect,DC=Contoso,DC=com" -ADconnectorAccount "Contoso\ADsyncSvc" -Verbose .EXAMPLE Export-ADSyncToolsADpermissionsReport -ADobjectDN "CN=User1,OU=AADconnect,DC=Contoso,DC=com" -ADconnectorAccountDN "CN=MSOL_11aabbcc1234,CN=Users,DC=Contoso,DC=com" -Verbose #> Function Export-ADSyncToolsADpermissionsReport { [CmdletBinding()] Param ( [Parameter(Mandatory=$true, Position=0)] $ADobjectDN, #TODO : change to ADconnectorAccountDN intergrate with search by DN [Parameter(Mandatory=$false, Position=1)] $ADconnectorAccount ) Write-Verbose "Enter: Export-ADSyncToolsADpermissionsReport" IsPowerShellSessionElevated IsAADConnectPresent Import-ADSyncToolsActiveDirectoryModule ## TODO integrate with Search by DN # Get the target objects from AD Try { $adObject = Get-ADObject $ADobjectDN -Properties $defaultADobjProperties -ErrorAction Stop $rootDN = [string] $adObject.DistinguishedName.Substring($adObject.DistinguishedName.IndexOf("DC=")) $adRoot = Get-ADObject $rootDN -Properties $defaultADobjProperties -ErrorAction Stop $forestRoot = (Get-ADDomain $rootDN).Forest } Catch { Throw "Cannot find AD object: $($_.Exception.Message)" } If ([string]::IsNullOrEmpty($ADconnectorAccount)) { # Get AD DS Connector Account from server config Write-Verbose "Get-ADSyncToolsADconnectorAccount" $connectorAccount = Get-ADSyncToolsADconnectorAccount | Where Forest -eq $forestRoot If ([string]::IsNullOrEmpty($connectorAccount)) { Throw "Cannot find AD Connector Space for user '$ADobjectDN'." } $ADconnectorAccount = $connectorAccount.Domain + "\" + $connectorAccount.Username } ## TODO: integrate with Search by Domain user $adConnectorAccObj = Get-ADSyncToolsADobjectByDomainUsername -DomainUsername $ADconnectorAccount # Get the ADSync service account # TODO: VSA scenario (SYSTEM security context) $adSyncSrvAccount = Get-ADSyncToolsServiceAccount If ($ADSyncSrvAccount.AccountType -eq 'VSA') { $adSyncSrvAccObj = $ADSyncSrvAccount } Else { $adSyncSrvAccObj = Get-ADSyncToolsADobjectByDomainUsername -DomainUsername $adSyncSrvAccount.ServiceLogOnAs } # Get Builtin container Try { $builtinDN = "CN=Builtin," + $rootDN $builtinObj = Get-ADObject $builtinDN -Properties $defaultADobjProperties -ErrorAction Stop } Catch { Throw "Cannot find AD Builtin Container: $($_.Exception.Message)" } # Get SAM Server object Try { $samServerDN = "CN=Server,CN=System," + $rootDN $samServerObj = Get-ADObject $samServerDN -Properties $defaultADobjProperties -ErrorAction Stop } Catch { Throw "Cannot find AD SAM Server object: $($_.Exception.Message)" } Export-ADSyncToolsADpermissions -ADtarget $adObject ` -ADroot $adRoot ` -ADconnectorAccount $adConnectorAccObj ` -ADSyncSrvAccount $adSyncSrvAccObj ` -ADbuiltinContainer $builtinObj ` -ADsamServer $samServerObj Write-Verbose "Exit: Export-ADSyncToolsADpermissionsReport" } <# .Synopsis Imports AD permissions data from XML file and returns a DACL table .DESCRIPTION This funtion takes as input the XML file containing DACL information obtained from 'Export-ADSyncToolsADpermissionsReport' cmdlet and returns an array of objects with each ACE. .EXAMPLE Import-ADSyncToolsADpermissionsReport -Path ".\ADSyncToolsExport_Contoso.com_-FullPerms.xml" #> Function Import-ADSyncToolsADpermissionsReport { [CmdletBinding()] Param ( # Permissions XML filename [Parameter(Mandatory=$true)] [string] $Path ) If (Test-Path $Path) { Try { Import-Clixml $Path } Catch { Throw "An error occurred reading the file '$Path'. Error Details: $($_.Exception.Message)" } } Else { Throw "File '$Path' not found." } } #endregion #======================================================================================= #======================================================================================= #region Migration / Disaster Recovery Functions #======================================================================================= <# .Synopsis Import ImmutableID from AAD .DESCRIPTION Generates a file with all Azure AD Synchronized users containing the ImmutableID value in GUID format Requirements: MSOnline PowerShell Module .EXAMPLE Import-ADSyncToolsSourceAnchor -OutputFile '.\AllSyncUsers.csv' .EXAMPLE Another example of how to use this cmdlet #> Function Import-ADSyncToolsSourceAnchor { [CmdletBinding()] Param ( # Output CSV file [Parameter(Mandatory=$true)] [String] $Output, # Get Synchronized Users from Azure AD Recycle Bin [Parameter(Mandatory=$false)] [switch] $IncludeSyncUsersFromRecycleBin = $false ) Try { $creds = Get-Credential # TODO : Support for AAD PowerShell v2 # TODO : Function to connect - Control connected state $tenantAzureEnvironment = Get-ADSyncToolsTenantAzureEnvironment $creds Connect-MsolService -Credential $creds -AzureEnvironment $tenantAzureEnvironment } Catch { Throw "Unable to Connect to Azure AD: $($_.Exception.Message)" } # Start Importing $results = @() $allSyncUsers = @() $userProperties = @('UserPrincipalName', 'ImmutableID', 'ObjectId', 'LastDirSyncTime', 'IsLicensed', 'SoftDeletionTimestamp') Write-Host "Reading Synchronized Users from Azure AD ..." $allSyncUsers = Get-MsolUser -Synchronized -All | Where-Object {$_.ImmutableID -ne $null} | select $userProperties If ($IncludeSyncUsersFromRecycleBin) { $allSyncUsers += Get-MsolUser -Synchronized -All -ReturnDeletedUsers | Where-Object {$_.ImmutableID -ne $null} | select $userProperties } # Start Processing #Write-Host "Found $($allSyncUsers.Count) Synchronized Users in Azure AD ..." Foreach ($user in $allSyncUsers) { # Convert ImmutableID to GUID for each user Try { $immutableIdGuid = [GUID] ([System.Convert]::FromBase64String($user.ImmutableID)) } Catch { # Failure to convert to GUID value - Skip to the next loop Write-Error "Failure to convert ImmutableID to a GUID string: $($_.Exception.Message)" Continue } # Instantiate custom Object with all the properties in $userProperties $aadUser = New-Object -TypeName PSObject $aadUser | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $($user.UserPrincipalName) $aadUser | Add-Member -MemberType NoteProperty -Name ImmutableID -Value $($user.ImmutableID) $aadUser | Add-Member -MemberType NoteProperty -Name ImmutableIdGuid -Value $immutableIdGuid $aadUser | Add-Member -MemberType NoteProperty -Name LastDirSyncTime -Value $($user.LastDirSyncTime) $aadUser | Add-Member -MemberType NoteProperty -Name IsLicensed -Value $($user.IsLicensed) $aadUser | Add-Member -MemberType NoteProperty -Name SoftDeletionTimestamp -Value $($user.SoftDeletionTimestamp) $aadUser | Select UserPrincipalName, ImmutableID, ImmutableIdGuid $results += $aadUser } # Exporting data Write-Host "`n`nExporting $($results.count) Synchronized Users in Azure AD ..." $results | Export-Csv "$Output.csv" -NoTypeInformation } <# .Synopsis Export ms-ds-Consistency-Guid Report .DESCRIPTION Generates a ms-ds-Consistency-Guid report based on an import CSV file from Import-ADSyncToolsSourceAnchor .EXAMPLE Import-Csv .\AllSyncUsers.csv | Export-ADSyncToolsSourceAnchorReport -Output ".\AllSyncUsers-Report" .EXAMPLE Another example of how to use this cmdlet #> Function Export-ADSyncToolsSourceAnchorReport { [CmdletBinding()] Param ( # Use Alternative Login ID (mail) [Parameter(Mandatory=$false)] [switch] $AlternativeLoginId = $false, # UserPrincipalName [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [String] $UserPrincipalName, # ImmutableIdGUID [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [String] $ImmutableIdGUID, # Output filename for CSV and LOG files [Parameter(Mandatory=$true)] [String] $Output ) Begin { Import-ADSyncToolsActiveDirectoryModule # Check/Remove output files $currentFolder = (Get-Location).Path Remove-Item "$currentFolder\$Output.csv" -ErrorAction SilentlyContinue -Confirm If (Test-Path "$currentFolder\$Output.csv") { Write-Error "File $currentFolder\$Output.csv already exists. Exiting." Exit } Remove-Item "$currentFolder\$Output.log" -ErrorAction SilentlyContinue -Confirm # Set the signInAttribute If ($AlternativeLoginId) { $signInAttribute = 'mail' } Else { $signInAttribute = 'UserPrincipalName' } $usersFound = $usersNotFound = 0 $defaultProperties = @('UserPrincipalName','ObjectGUID','mS-DS-ConsistencyGuid','distinguishedName') } Process { #$objectResult = $msgResult = $seachResult = $null $logMessage = "AD user with $UserPrincipalName in $signInAttribute" + "`t" + "ImmutableId: $ImmutableIdGUID" $seachResult = $null # Initiate custom object $objectResult = New-Object -TypeName PSObject $objectResult | Add-Member -MemberType NoteProperty -Name UserPrincipalName -Value $UserPrincipalName $objectResult | Add-Member -MemberType NoteProperty -Name ImmutableIdGUID -Value $ImmutableIdGUID $objectResult | Add-Member -MemberType NoteProperty -Name OnPremisesUPN -Value $null $objectResult | Add-Member -MemberType NoteProperty -Name ObjectGUID -Value $null $objectResult | Add-Member -MemberType NoteProperty -Name ConsistencyGuid -Value $null $objectResult | Add-Member -MemberType NoteProperty -Name SearchResult -Value $null $objectResult | Add-Member -MemberType NoteProperty -Name Action -Value $null $objectResult | Add-Member -MemberType NoteProperty -Name Description -Value $null $objectResult | Add-Member -MemberType NoteProperty -Name DistinguishedName -Value $null Write-Verbose "UserPrincipalName : $UserPrincipalName | ImmutableIdGUID : $ImmutableIdGUID" # Search for User in AD Try { $user = Get-ADObject -Filter '$signInAttribute -eq $UserPrincipalName' -Properties $defaultProperties -ErrorAction Stop } Catch { Write-Error "Unable to search in ActiveDirectory: $($_.Exception.Message)" return } # User not found searching for the UserPrincipalName If ($user -eq $null) { $seachResult = "UserPrincipalName does not exist in AD" $objectResult.OnPremisesUPN = "N/A" $objectResult.ObjectGUID = "N/A" $objectResult.ConsistencyGuid = "N/A" $objectResult.SearchResult = $seachResult $objectResult.Action = "Skip" $objectResult.Description = "AD User cannot be found" $objectResult.DistinguishedName = "N/A" $usersNotFound ++ # Try to search for the UPN prefix in sAMAccountName, if not using Alternative LoginId If (-not $AlternativeLoginId) { # Get the UPN prefix Try { $upnPrefix = $UserPrincipalName.Substring(0, $UserPrincipalName.IndexOf('@')) } Catch { Write-Error "Invalid UserPrincipalName format: $($_.Exception.Message)" } # Search for UPN prefix on AD sAMAccountName If ($upnPrefix -notlike "") { Try { $user = Get-ADObject -Filter 'sAMAccountName -eq $upnPrefix' -Properties $defaultProperties -ErrorAction Stop } Catch { Write-Error "Unable to search in ActiveDirectory: $($_.Exception.Message)" return } # User Found in AD based on the UPN prefix equal to AD sAMAccountName If ($user -ne $null) { $seachResult = "UserPrincipalName prefix present in AD sAMAccountName" $objectResult.OnPremisesUPN = $user.UserPrincipalName $objectResult.ObjectGUID = $user.ObjectGUID $objectResult.ConsistencyGuid = Get-ADSyncToolsMsDsConsistencyGuid($user) $objectResult.SearchResult = $seachResult $objectResult.Action = "" $objectResult.Description = "" $objectResult.DistinguishedName = $user.distinguishedName $usersFound ++ $usersNotFound -- } } } $logMessage += "`t" + "Result: $seachResult" } Else { # User Found in AD $seachResult = "UserPrincipalName is present in AD" $objectResult.OnPremisesUPN = $user.UserPrincipalName $objectResult.ObjectGUID = $user.ObjectGUID $objectResult.ConsistencyGuid = Get-ADSyncToolsMsDsConsistencyGuid($user) $objectResult.SearchResult = $seachResult $objectResult.DistinguishedName = $user.distinguishedName $logMessage += "`t" + "Result: $seachResult" $usersFound ++ $userFound = $true } # Calculate Action + Action Result If ($user -ne $null) { If ($objectResult.ConsistencyGuid -eq $null) { # Target AD User does not have a ConsistencyGuid value yet $objectResult.Action = "Add" $objectResult.Description = "AD User does not have 'mS-DS-ConsistencyGuid' value" } Else { # Compare AAD ImmutableId with AD 'mS-DS-ConsistencyGuid' values $AdObject = $objectResult | Select ImmutableIdGUID, ConsistencyGuid $sourceConsistencyGuid = [GUID] $AdObject.ImmutableIdGUID $targetConsistencyGuid = [GUID] $AdObject.ConsistencyGuid Write-Verbose "sourceConsistencyGuid : $sourceConsistencyGuid | targetConsistencyGuid : $targetConsistencyGuid" If ($sourceConsistencyGuid -eq $targetConsistencyGuid) { $objectResult.Action = "Skip" $objectResult.Description = "AD User already have the correct 'mS-DS-ConsistencyGuid'" } Else { $objectResult.Action = "Update" $objectResult.Description = "AD User requires an update of 'mS-DS-ConsistencyGuid'" } } } #$objectResult | Select UserPrincipalName, ImmutableIdGUID, ConsistencyGuid, SearchResult, Action, Description $objectResult | Select UserPrincipalName, SearchResult, Action, Description $objectResult | Export-Csv "$currentFolder\$Output.csv" -NoTypeInformation -Append -Delimiter "`t" $logMessage + "`n" | Out-File "$currentFolder\$Output.log" -Append } End { Write-Host "`n`nProcessed a total of $($usersFound + $usersNotFound) users | $usersFound Users found + $usersNotFound Users not found" Write-Host "Report file: $currentFolder\$Output.csv" Write-Host "Log file: $currentFolder\$Output.log" } } <# .Synopsis Updates users with the new ConsistencyGuid (ImmutableId) .DESCRIPTION Updates users with the new ConsistencyGuid (ImmutableId) value taken from the ConsistencyGuid Report This function supports the WhatIf switch Note: ConsistencyGuid Report must be imported with Tab delimiter .EXAMPLE Import-Csv .\AllSyncUsersTEST-Report.csv -Delimiter "`t"| Update-ADSyncToolsSourceAnchor -Output .\AllSyncUsersTEST-Result2 -WhatIf .EXAMPLE Import-Csv .\AllSyncUsersTEST-Report.csv -Delimiter "`t"| Update-ADSyncToolsSourceAnchor -Output .\AllSyncUsersTEST-Result2 #> Function Update-ADSyncToolsSourceAnchor { [CmdletBinding(SupportsShouldProcess)] Param ( # DistinguishedName [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [String] $DistinguishedName = $false, # ImmutableIdGUID [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [String] $ImmutableIdGUID, # Action [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [String] $Action, # Output filename for LOG files [Parameter(Mandatory=$true)] [String] $Output ) Begin { Import-ADSyncToolsActiveDirectoryModule # Check/Remove output files $currentFolder = (Get-Location).Path Remove-Item "$currentFolder\$Output.log" -ErrorAction SilentlyContinue -Confirm Write-Verbose "WhatIfPreference: $WhatIfPreference" } Process { # Process each user with Add or Update action Write-Verbose "$Action | Value:$ImmutableIdGUID | User:$DistinguishedName" If ($Action -eq 'Add' -or $Action -eq 'Update') { $logMessage = "$Action | Value:$ImmutableIdGUID | User:$DistinguishedName" Write-Host $logMessage $adObject = Search-ADSyncToolsADobject -User $DistinguishedName If ($adObject) { Try { if ($PSCmdlet.ShouldProcess($adObject, $Action)) { $newValue = [GUID] $ImmutableIdGUID Set-ADObject -Identity $adObject -Replace @{'mS-DS-ConsistencyGuid'=$newValue} $logMessage += " | Result: Success" $usersUpdated ++ } } Catch { # Could not set user Write-Error "Unable to set user $adObject in Active Directory: $($_.Exception.Message)" $logMessage += " | Result: Failed" $usersFailed ++ } } $logMessage + "`n" | Out-File "$currentFolder\$Output.log" -Append } } End { Write-Host "`n`nProcessed a total of $($usersUpdated + $usersFailed) users | $usersUpdated Users Updated + $usersFailed Users Failed" Write-Host "Log file: $currentFolder\$Output.log" } } #endregion #======================================================================================= #======================================================================================= #region Mitigation Functions #======================================================================================= <# .Synopsis Repair Azure AD Connect AutoUpgrade State .DESCRIPTION Fixes an issue with AutoUpgrade introduced in build 1.1.524.0 (May 2017) which disables the online checking of new versions while AutoUpgrade is enabled. .EXAMPLE Repair-ADSyncToolsAutoUpgradeState #> Function Repair-ADSyncToolsAutoUpgradeState { [CmdletBinding()] Param() IsAADConnectPresent -MinVersion '1.1.524.0' IsAADConnectPresent -MaxVersion '1.3.20.0' IsPowerShellSessionElevated # Checking UpdateCheckEnabled Registry key $regkey = 'HKLM:\SOFTWARE\Microsoft\ADHealthAgent\Sync' $name = 'UpdateCheckEnabled' Try { $val = Get-ItemProperty -Path $regkey -Name $name -ErrorAction Stop Write-Host 'UpdateCheckEnabled: ' $val.UpdateCheckEnabled } Catch [System.Security.SecurityException] { Write-Error "Please execute this cmdlet in Windows PowerShell with 'Run As Administrator': $($_.Exception.Message)" Return } # Checking ADSync AutoUpgrade Status Try { $autoUpgradeState = Get-ADSyncAutoUpgrade -ErrorAction Stop Write-Host 'ADSyncAutoUpgrade: ' $autoUpgradeState } Catch { Write-Error "Error retrieving ADSync AutoUpgrade status: $($_.Exception.Message)" Return } # Checking AutoUpgrade State Fix $isAgentDisabled = $val.UpdateCheckEnabled -eq 0 $isAutoUpgradeAllowed = $autoUpgradeState -ne 'disabled' If($isAutoUpgradeAllowed -and $isAgentDisabled) { # Applying AutoUpgrade Fix Write-Host 'Fixing AutoUpgrade status and restarting AutoUpgrade service...' Set-ItemProperty -path $regkey -name $name -value 1 Restart-Service 'AzureADConnectHealthSyncMonitor' Write-Host 'Result: AutoUpgrade has been fixed successfully.' } Else { # Skipping AutoUpgrade Fix Write-Host 'Result: AutoUpgrade fix is not required.' } } Function Get-ADSyncToolsTls12RegValue { [CmdletBinding()] Param ( # Registry Path [Parameter(Mandatory=$true, Position=0)] [string] $RegPath, # Registry Name [Parameter(Mandatory=$true, Position=1)] [string] $RegName ) $regItem = Get-ItemProperty -Path $RegPath -Name $RegName -ErrorAction Ignore $regKey = Get-Item -Path $RegPath -ErrorAction Ignore $output = "" | select Path, Name, Value, Type $output.Path = $RegPath $output.Name = $RegName If ($regItem -eq $null) { $output.Value = "Not Found" $output.Type = "N/A" } Else { $output.Value = $regItem.$RegName $output.Type = $regKey.GetValueKind($RegName) } $output } <# .Synopsis Gets Client\Server TLS 1.2 settings for .NET Framework .DESCRIPTION Reads information from the Registry regarding TLS 1.2 for .NET Framework: Path Name ---- ---- HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319 SystemDefaultTlsVersions HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319 SchUseStrongCrypto HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319 SystemDefaultTlsVersions HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319 SchUseStrongCrypto HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server Enabled HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server DisabledByDefault HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client Enabled HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client DisabledByDefault .EXAMPLE Get-ADSyncToolsTls12 .LINK More Information: TLS 1.2 enforcement for Azure AD Connect https://docs.microsoft.com/en-us/azure/active-directory/hybrid/reference-connect-tls-enforcement #> Function Get-ADSyncToolsTls12 { [CmdletBinding()] Param () $regSettings = @() $regKey = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'SystemDefaultTlsVersions' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'SchUseStrongCrypto' $regKey = 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'SystemDefaultTlsVersions' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'SchUseStrongCrypto' $regKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'Enabled' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'DisabledByDefault' $regKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'Enabled' $regSettings += Get-ADSyncToolsTls12RegValue $regKey 'DisabledByDefault' $regSettings } <# .Synopsis Sets Client\Server TLS 1.2 settings for .NET Framework .DESCRIPTION Sets the registry entries to enable/disable TLS 1.2 for .NET Framework: Path Name ---- ---- HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319 SystemDefaultTlsVersions HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319 SchUseStrongCrypto HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319 SystemDefaultTlsVersions HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319 SchUseStrongCrypto HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server Enabled HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server DisabledByDefault HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client Enabled HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client DisabledByDefault Running the cmdlet without any parameters will enable TLS 1.2 for .NET Framework More Information: TLS 1.2 enforcement for Azure AD Connect https://docs.microsoft.com/en-us/azure/active-directory/hybrid/reference-connect-tls-enforcement .EXAMPLE Set-ADSyncToolsTls12 .EXAMPLE Set-ADSyncToolsTls12 -Enabled $true #> Function Set-ADSyncToolsTls12 { [CmdletBinding()] Param ( # TLS 1.2 Enabled [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [bool] $Enabled = $true ) $ErrorActionPreference = 'Stop' Write-Warning 'Modifying TLS settings may affect other services on the Server.' -WarningAction Inquire If ($Enabled) { $regValueEnabled = '1' $regValueDisabled = '0' $message = 'TLS 1.2 has been enabled. You must restart the Windows Server for the changes to take affect.' } Else { $regValueEnabled = '0' $regValueDisabled = '1' $message = 'TLS 1.2 has been disabled. You must restart the Windows Server for the changes to take affect.' } Try { If (-Not (Test-Path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319')) { New-Item 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' -Force | Out-Null } New-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' -Name 'SystemDefaultTlsVersions' -Value $regValueEnabled -PropertyType 'DWord' -Force | Out-Null New-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value $regValueEnabled -PropertyType 'DWord' -Force | Out-Null If (-Not (Test-Path 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319')) { New-Item 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' -Force | Out-Null } New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' -Name 'SystemDefaultTlsVersions' -Value $regValueEnabled -PropertyType 'DWord' -Force | Out-Null New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value $regValueEnabled -PropertyType 'DWord' -Force | Out-Null If (-Not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server')) { New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Force | Out-Null } New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'Enabled' -Value $regValueEnabled -PropertyType 'DWord' -Force | Out-Null New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'DisabledByDefault' -Value $regValueDisabled -PropertyType 'DWord' -Force | Out-Null If (-Not (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client')) { New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Force | Out-Null } New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Name 'Enabled' -Value $regValueEnabled -PropertyType 'DWord' -Force | Out-Null New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Name 'DisabledByDefault' -Value $regValueDisabled -PropertyType 'DWord' -Force | Out-Null } Catch [System.Security.SecurityException] { Throw "Please execute this cmdlet in Windows PowerShell with 'Run As Administrator': $($_.Exception.Message)" } Catch { Throw "Failed to set Registry settings. Error Details: $($_.Exception.Message)" } Write-Host $message -ForegroundColor Cyan } #endregion #======================================================================================= #======================================================================================= #region SQL Functions #======================================================================================= Function GetInnerExceptionMessage { Param ( $exception ) $innerException = $exception.InnerException If ($innerException) { return $innerException.Message } Return $null; } Function SplitString { Param ( [string] $Source, [string] $SplitCharacter, [switch] $RemoveDuplicates ) If ($RemoveDuplicates) { $splitOption = [System.StringSplitOptions]::RemoveEmptyEntries } Else { $splitOption = [System.StringSplitOptions]::None } $records = $Source.Split($SplitCharacter, $splitOption) Return $records } Function ParseBrowserResponse { Param ( [string] $ResponseString ) # A SQL Browser response looks like instance;;instance;;instance;; where each instance string # contains a list of parameter/value pairs encoded as: p1;v1;p2;v2;p3;v3;p4;v4;; $response = $ResponseString.Substring(3,$ResponseString.Length-3).Replace(";;","~") $instanceRecords = SplitString -Source $response -SplitCharacter "~" -RemoveDuplicates Write-Host SQL browser response contained $instanceRecords.Length instances. $Instances = @(); ForEach ($instance in $instanceRecords) { $sqlInstance = New-Object -TypeName PsObject $config = SplitString -Source $instance -SplitCharacter ";" $param = 0 $sqlInstance | Add-Member -MemberType NoteProperty -Name BrowserRecord -Value $instance For ($param = 0; $param -lt $config.Count; $param + 2) { $keyword = $config[$param] $value = $config[$param + 1] $sqlInstance | Add-Member -MemberType NoteProperty -Name $keyword -Value $value $param += 2 } $instances += $sqlInstance } Return $instances } <# .Synopsis Get SQL Server Instances from SQL Browser service .DESCRIPTION SQL Diagnostics related functions and utilities .EXAMPLE Get-ADSyncToolsSqlBrowserInstances -Server 'sqlserver01' #> Function Get-ADSyncToolsSqlBrowserInstances { Param ( # SQL Server Name [string] $Server ) $Port = 1434 $ConnectionTimeout = 1000 $UDPClient = new-Object system.Net.Sockets.Udpclient $UDPClient.client.ReceiveTimeout = $ConnectionTimeout $UDPClient.Client.Blocking = $True Write-Progress -Activity "Attempting to retrieve instance information for '$Server'" -Status "Querying the SQL Server Browser service" Try { $UDPClient.Connect($Server, $Port) } Catch { $innerExceptionMsg = GetInnerExceptionMessage($_.Exception) Write-Error -Category ConnectionError "Unable to connect to the SQL Server Browser service on $Server port $Port (UDP). $innerExceptionMsg." Return $null } $rawResponse = "" Try { $UDPPacket = 0x02,0x00,0x00 $UDPEndpoint = New-Object System.Net.IPEndPoint([system.net.ipaddress]::Any,0) [void]$UDPClient.Send($UDPPacket, $UDPPacket.length) $BytesRecived = $UDPClient.Receive([ref]$UDPEndpoint) $ToASCII = New-Object System.Text.ASCIIEncoding $rawResponse = $ToASCII.GetString($BytesRecived) $socket = $null $UDPClient.Close() } Catch { Write-Progress -Activity "Attempting to retrieve instance information for $Server" -Status "Failed" -Completed $innerExceptionMsg = GetInnerExceptionMessage($_.Exception) $message = "Unable to read the SQL Server Browser configuration. " $message += $innerExceptionMsg + ". " $message += "Ensure port $port (UDP) is open on $Server and the SQL Server Browser service is running. " Write-Error -Category ConnectionError $message $UDPClient.Close() Return $null } If ($rawResponse) { $instances = @(ParseBrowserResponse -ResponseString $rawResponse) Write-Host "Verifying protocol bindings and port connectivity..." $step = 0 ForEach ($instance in $Instances) { $instanceName = $instance.InstanceName $port = $instance.tcp $step++ $complete = ($step * 100) / $instances.Count If ($instance.tcp) { Write-Progress -Activity "Verifying SQL Browser Configuration" -Status "Instance: $instanceName - connecting to port $port" -PercentComplete $complete $isPortOpen = Test-ADSyncToolsSqlNetworkPort -Server $Server -Port $instance.tcp if ($isPortOpen) { $status = "Enabled - port $port is assigned and reachable through the firewall" $instance | Add-Member -MemberType NoteProperty -Name TcpStatus -Value $status Start-Sleep -Seconds 2 } Else { $status = "Blocked - the inbound firewall rule for port $port is missing or disabled" $instance | Add-Member -MemberType NoteProperty -Name TcpStatus -Value $status } } Else { Write-Progress -Activity "Verifying SQL Browser Configuration" -Status "Instance: $instanceName - TCP/IP binding is disabled" -PercentComplete $complete $status = "Disabled - the TCP/IP binding for this instance is missing or disabled" $instance | Add-Member -MemberType NoteProperty -Name tcp -Value Disabled $instance | Add-Member -MemberType NoteProperty -Name TcpStatus -Value $status Start-Sleep -Seconds 2 } $progressMsg = "{0,-15} : {1}" -f $instanceName,$status Write-Host $progressMsg } Write-Progress -Activity "Verifying SQL Firewall Configuration" -Status "Completed" -Completed Return $Instances } Return $null } <# .Synopsis Gets the status of SQL Server Protocols .DESCRIPTION Displays all SQL Server Protocols status running on the SQL Server. SQL LocalDB is not supported. .EXAMPLE Get-ADSyncToolsSqlProtocols #> function Get-ADSyncToolsSqlProtocols { [CmdletBinding()] Param () # Check if module is installed Try { Import-Module SQLPS -ErrorAction Stop } Catch { Throw "This function requires SQL PowerShell module installed (SQLPS)" } # Create a SQL Management object Try { $smo = 'Microsoft.SqlServer.Management.Smo.' $wmi = New-Object ($smo + 'Wmi.ManagedComputer') } Catch { Throw "There was an error creating SQL Management object. Error Details: $($_.Exception.Message)" } # Show SQL Server settings "SQL Server Configuration:" $wmi # Show SQL Client Protocols "SQL Client Protocols:" $wmi.ClientProtocols | select DisplayName,IsEnabled,Urn | fl "`nSQL Named Pipes Protocol:" $urnClientNp = ($wmi.ClientProtocols | where Name -eq 'np').Urn.Value $clientNp = $wmi.GetSmoObject($urnClientNp) $clientNp | select DisplayName,Name,IsEnabled,NetworkLibrary | fl # Show SQL Server Protocols $serverInstances = @($wmi.ServerInstances | %{$_ | select Name,ServerProtocols,Urn}) "SQL Server Protocols:" ForEach ($instance in $serverInstances) { $instance $serverProtocol = $instance.ServerProtocols | where Name -eq 'Np' | select DisplayName,Name,IsEnabled,` @{Name='Enabled'; Expression={$_.ProtocolProperties['Enabled'].Value}},` @{Name='PipeName'; Expression={$_.ProtocolProperties['PipeName'].Value}} $serverProtocol | fl } } <# .Synopsis Test the SQL Server network port .DESCRIPTION SQL Diagnostics related functions and utilities .EXAMPLE Test-ADSyncToolsSqlNetworkPort -Server 'sqlserver01' .EXAMPLE Test-ADSyncToolsSqlNetworkPort -Server 'sqlserver01' -Port 1433 #> Function Test-ADSyncToolsSqlNetworkPort { Param ( # SQL Server Name [string] $Server, # SQL Server Port [string] $Port ) $tcpClient = New-Object Net.Sockets.TcpClient Try { $tcpClient.Connect($Server, $Port) } Catch {} If ($tcpClient.Connected) { $tcpClient.Close() Return $true } Return $false } <# .Synopsis Resolve a SQL server name .DESCRIPTION SQL Diagnostics related functions and utilities .EXAMPLE Resolve-ADSyncToolsSqlHostAddress -Server 'sqlserver01' #> Function Resolve-ADSyncToolsSqlHostAddress { Param ( # SQL Server Name [Parameter(Mandatory=$true)] [string] $Server ) Try { Write-Host Resolving server address : $Server $ipAddresses = [System.Net.Dns]::GetHostAddresses($Server) | Select-Object -Property AddressFamily, IPAddressToString foreach ($address in $ipAddresses) { Write-Host " $($address.AddressFamily): $($address.IPAddressToString) `n" } Return $ipAddresses } Catch { $innerExceptionMsg = GetInnerExceptionMessage($_.Exception) Write-Error -Category ObjectNotFound "Unable to resolve host address '$Server'. Error Details: $innerExceptionMsg" Return $null } } <# .Synopsis Connect to a SQL database for testing purposes .DESCRIPTION SQL Diagnostics related functions and utilities .EXAMPLE Connect-ADSyncToolsSqlDatabase -Server 'sqlserver01.contoso.com' -Database 'ADSync' .EXAMPLE Connect-ADSyncToolsSqlDatabase -Server 'sqlserver01.contoso.com' -Instance 'INSTANCE01' -Database 'ADSync' #> Function Connect-ADSyncToolsSqlDatabase { Param ( # SQL Server Name [Parameter(Mandatory=$true)] [string] $Server, # SQL Server Instance Name [string] $Instance, # SQL Server Database Name [string] $Database, # SQL Server Port (e.g. 49823) [string] $Port, # SQL Server Login Username [string] $UserName, # SQL Server Login Password [string] $Password ) $ErrorActionPreference = 'Continue' $sqlConfigMgr = "SQL Server Configuration Manager" # Bail immediately if we can't resolve the server name $ipAddresses = Resolve-ADSyncToolsSqlHostAddress -Server $Server if (-not $ipAddresses) { Return } # Try connecting over TCP using the full instance name + optional port ex: "MySqlInstance,1234" # If this succeeds return the connection object for use in SQL queries $sqlTcpConnection = New-ADSyncToolsSqlConnection ` -Server $Server ` -Instance $Instance ` -Database $Database ` -Protocol 'tcp' ` -Port $Port ` -UserName $UserName ` -Password $Password If ($Instance) { Write-Host Attempting to connect to $Server\$Instance using a TCP binding. } Else { Write-Host Attempting to connect to $Server using a TCP binding for the default instance. } Write-Host " $($sqlTcpConnection.ConnectionString)" Write-Progress -Activity "Connecting to $Instance on $Server" -Status "Attempting TCP/IP connection" Try { $sqlTcpConnection.Open() Write-Host " Successfully connected." Return $sqlTcpConnection } Catch { $innerExceptionMsg = GetInnerExceptionMessage($_.Exception) Write-Error -Category ConnectionError "Unable to connect using a TCP binding. $innerExceptionMsg `n" } # Parse out the SQL instance name and port as the latter only makes sense for TCP connections Write-Progress -Activity "Connecting to $Instance on $Server" -Completed $instanceName = $Instance $port = $null if ($Instance) { $instanceParams = SplitString -Source $Instance -SplitCharacter "," -RemoveDuplicates if ($instanceParams.Count -eq 2) { $instanceName = $instanceParams[0] $port = $instanceParams[1] } } # Fall thru to troubleshooting / diagnostic steps Write-Host "TROUBLESHOOTING: Attempting to query the SQL Server Browser service configuration on $Server. `n" $instances = Get-ADSyncToolsSqlBrowserInstances -Server $Server # The SQL browser tells us all enabled protocols and ports. Whip thru the list testing the # TCP ports to see if they can be successfully opened. A failure here is most likely due to # a missing inbound firewall rule. Write-Host "`nWHAT TO TRY NEXT: `n" If ($instances) { $sqlBrowserEnabled = $true [string] $message = "Each SQL instance must be bound to an explicit static TCP port and paired with an " $message += "inbound firewall rule on $Server to allow connection. Review the TcpStatus field " $message += "for each instance and take corrective action. `n" Write-Host $message $instances | Format-List -Property InstanceName,tcp,TcpStatus } # The browser isn't running so the best we can do is give some advice and probe the port to see # if it can be successfully opened. The user should use the SQL Server Configuration Manager to # verify the protocol bindings and/or start the SQL Server Browser service give this script more # information to further troubleshoot the issue. Else { $sqlBrowserEnabled = $false $message = "Each SQL instance must be bound to an explicit static TCP port and paired with an inbound firewall rule on $Server to allow connection. " $message += "Enable the SQL Server Browser service temporarily on the SQL server and use Get-ADSyncToolsSqlBrowserInstances to further troubleshoot the issue. " $message += "Alternatively use the $sqlConfigMgr on $Server to verify the instance name and TCP/IP port assignment manually. `n" Write-Host $message # If no instance was given we test the typical port for the DEFAULT SQL instance (TCP 1433). If we can't # connect then most likely they are missing a firewall rule OR have tinkered with the default port. If (-not $Instance) { $portRequired = $false Write-Host "Determining if the default SQL instance port (TCP 1433) is open on" $Server. $portOpen = Test-ADSyncToolsSqlNetworkPort -Server $Server -Port 1433 If ($portOpen) { Write-Host " " The port for the default SQL instance is open. } Else { $message = "The typical port for the DEFAULT SQL instance (TCP 1433) is not open. Use the $sqlConfigMgr to verify " $message += "the SQL configuration and ensure an inbound firewall rule is opened on $Server. `n" Write-Host $message } } # For instances other than the default, both the name + port must be given in order to connect Else { $portRequired = $true # With no browser, the SQL client won't be able to connect unless we specify the port number! If (!$port) { $message = "You must specify both the instance name and the port to connect when the SQL Server Browser service is not running. " $message += "An inbound firewall rule on $Server is required for the associated port.`n" $message += "`tExample: 'MySQLInstance,1234' where 1234 has a matching firewall rule." Write-Host $message } # Test whether or not we can open the specified port. If we can't then most likely the firewall rule is missing. Else { Write-Host "To connect to the $instanceName instance, $Server must have an inbound firewall rule for port $port." Write-Progress -Activity "Verifying network connectivity" -Status "Instance: $instanceName - connecting to port $port" Write-Host "Verifying port $port on $Server is open." $portOpen = Test-ADSyncToolsSqlNetworkPort -Server $Server -Port $port If ($portOpen) { Write-Host "Successfully probed port. `n" } Else { Write-Host "Unable to open port $port. `n" $message = "Use the $sqlConfigMgr on $Server to verify the instance name and port assignment. " $message += "Then verify an associated inbound firewall rule is opened on $Server. `n" Write-Host $message } Write-Progress -Activity "Verifying network connectivity" -Completed } } } } <# .Synopsis Invoke a SQL query against a database for testing purposes .DESCRIPTION SQL Diagnostics related functions and utilities .EXAMPLE New-ADSyncToolsSqlConnection -Server SQLserver01.Contoso.com -Port 49823 | Invoke-ADSyncToolsSqlQuery .EXAMPLE $sqlConn = New-ADSyncToolsSqlConnection -Server SQLserver01.Contoso.com -Port 49823 Invoke-ADSyncToolsSqlQuery -SqlConnection $sqlConn -Query 'SELECT *, database_id FROM sys.databases' #> Function Invoke-ADSyncToolsSqlQuery { Param ( # SQL Connection [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [System.Data.SqlClient.SqlConnection] $SqlConnection, # SQL Query [Parameter(Mandatory=$false, Position=1)] [string] $Query = "SELECT name, database_id FROM sys.databases" ) $command = New-Object System.Data.SqlClient.SqlCommand($Query, $SqlConnection) $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($command) $dataset = New-Object System.Data.DataSet Try { $rows = $adapter.Fill($dataSet) } Catch { Write-Error -Category InvalidOperation "Query failed. $_.Exception.Message" Return $null } Write-Host "Query returned $rows rows." Return $dataSet.Tables } <# .Synopsis Create a SQL client connection .DESCRIPTION SQL Diagnostics related functions and utilities .EXAMPLE New-ADSyncToolsSqlConnection -Server SQLserver01.Contoso.com .EXAMPLE New-ADSyncToolsSqlConnection -Server SQLserver01.Contoso.com -Port 49823 .EXAMPLE New-ADSyncToolsSqlConnection -Server SQLserver01.Contoso.com -Database ADSync -Instance AADCONNECT1 -Port 49823 -Protocol tcp #> Function New-ADSyncToolsSqlConnection { Param ( # SQL Server Name [Parameter(Mandatory=$true)] [string] $Server, # SQL Server Instance Name [string] $Instance, # SQL Server Database Name [string] $Database, # SQL Server Protocol (e.g. tcp) [string] $Protocol, # SQL Server Port (e.g. 49823) [string] $Port, # SQL Server Login Username [string] $UserName, # SQL Server Login Password [string] $Password ) $sqlBinding = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $sqlBinding['Integrated Security'] = $true # $sqlBinding['Connect Timeout'] = 30 If ($Port) { $ServerBinding = "$Server,$Port" } Else { $ServerBinding = "$Server" } If ($Protocol) { $sqlBinding['Data Source'] = "${Protocol}:$ServerBinding\$Instance" } Else { $sqlBinding['Data Source'] = "$ServerBinding\$Instance" } If ($Database) { $sqlBinding['Initial Catalog'] = $Database } If ($UserName) { $sqlBinding['User ID'] = $UserName } If ($Password) { $sqlBinding['Password'] = $Password } $sqlConnection = New-Object System.Data.SqlClient.SqlConnection ($sqlBinding) Return $sqlConnection } #endregion #======================================================================================= #======================================================================================= #region Duplicate Users SourceAnchor Tool #======================================================================================= Function Get-ADSyncToolsDuplicateUsersSourceAnchor { [CmdletBinding()] Param ( # AD connector name for which user source anchors needs to be repaired [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] $ADConnectorName ) Write-Verbose "Entering: Get-ADSyncToolsDuplicateUsersSourceAnchor" IsAADConnectPresent # Import Modules Import-ADSyncToolsActiveDirectoryModule $aadConnectRegistryKey = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Azure AD Connect' $modulePath = [System.IO.Path]::Combine($aadConnectRegistryKey.InstallationPath, "AADPowerShell\MSOnline.psd1") Try { Import-Module $modulePath -ErrorAction Stop } Catch { Throw "Failed to import MSOnline module from '$modulePath'. Error Details: $($_.Exception.Message)" } # Run csexport.exe $exportFilePath = $env:temp + "\export.xml" $exportFilePathArgs = " " + $exportFilePath + " /f:i" $csExportFilePath = Join-Path -Path $(Get-ADSyncToolsADsyncFolder) -ChildPath 'Bin\csexport.exe' If (Test-Path $exportFilePath) { Remove-Item -Path $exportFilePath } Start-Process -FilePath $csExportFilePath -ArgumentList `"$ADConnectorName`",$exportFilePathArgs -Wait Try { [xml] $content = Get-Content -Path $exportFilePath -ErrorAction Stop } Catch { Write-Host "No synchronization errors present in connector space '$ADConnectorName'." -ForegroundColor Cyan } # Process sync errors $exportErrorObjects = $content.SelectNodes("cs-objects/cs-object") $applyFixForAllObjects = $false $results = @() ForEach ($exportErrorObjectInfo in $exportErrorObjects) { $callStackInfo = $exportErrorObjectInfo.SelectSingleNode("import-errordetail/import-status/extension-error-info/call-stack").InnerText $exportErrorObject = $exportErrorObjectInfo.SelectSingleNode("synchronized-hologram/entry") $adDomainName = $exportErrorObjectInfo.SelectSingleNode("fully-qualified-domain-name").InnerText If ($callStackInfo -eq $null -or $exportErrorObject -eq $null -or $callStackInfo -NotMatch "SourceAnchor attribute has changed.") { Continue } $csObject = Get-ADSyncCSObject -DistinguishedName $exportErrorObject.dn -ConnectorName $ADConnectorName $mvObject = Get-ADSyncMVObject -Identifier $csObject.ConnectedMVObjectId If ($csObject -and $mvObject -and $adDomainName -and ` $mvObject.Attributes['sourceAnchor'] -and $mvObject.Attributes['sourceAnchor'].Values[0]) { $duplicateUserSourceAnchorInfo = [DuplicateUserSourceAnchorInfo]::new() $duplicateUserSourceAnchorInfo.UserName = $csObject.Attributes['displayName'].Values[0] $duplicateUserSourceAnchorInfo.DistinguishedName = $exportErrorObject.dn $duplicateUserSourceAnchorInfo.ADDomainName = $adDomainName Try { $duplicateUserSourceAnchorInfo.CurrentMsDsConsistencyGuid = [System.Convert]::FromBase64String($csObject.Attributes['mS-DS-ConsistencyGuid'].Values[0]) $duplicateUserSourceAnchorInfo.ExpectedMsDsConsistencyGuid = [System.Convert]::FromBase64String($mvObject.Attributes['sourceAnchor'].Values[0]) $results += [pscustomobject]@{ DuplicateUserSourceAnchorInfo = $duplicateUserSourceAnchorInfo } } Catch { Write-Host "Unable to parse MsDsConsistencyGuid value for `"$($DuplicateUserSourceAnchorInfo.UserName)`"" } } } $results | ForEach-Object -MemberName DuplicateUserSourceAnchorInfo Write-Verbose "Exiting: Get-ADSyncToolsDuplicateUsersSourceAnchor" } Function Set-ADSyncToolsDuplicateUsersSourceAnchor { [CmdletBinding()] Param ( # User list for which the source anchor needs to be fixed [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [DuplicateUserSourceAnchorInfo] $DuplicateUserSourceAnchorInfo, # AD EA/DA Admin Credentials, If not provided default credentials will be used [Parameter(Mandatory=$false)] [PSCredential] $ActiveDirectoryCredential, [Parameter(Mandatory=$false)] [bool] $OverridePrompt = $false ) Begin { Write-Verbose "Entering: Set-ADSyncToolsDuplicateUsersSourceAnchor" $abort = $false } Process { If (-not $abort) { $decision = 0 Write-Host If ($OverridePrompt -eq $false) { $infoUsername = $DuplicateUserSourceAnchorInfo.UserName $infoDN = $DuplicateUserSourceAnchorInfo.DistinguishedName $infoCurrentGuid = [guid] $DuplicateUserSourceAnchorInfo.CurrentMsDsConsistencyGuid $infoExpectedGuid = [guid] $DuplicateUserSourceAnchorInfo.ExpectedMsDsConsistencyGuid $question = "'$infoUsername' mS-DS-ConsistencyGuid value will be updated from '$infoCurrentGuid' to '$infoExpectedGuid'. Do you want to continue?" $choices = '&Yes', '&No', '&Cancel' Write-Verbose "Prompting to update SourceAnchor for object '$infoDN'..." $decision = $Host.UI.PromptForChoice($title, $question, $choices, 1) } If ($decision -eq 0) { Write-Host "Updating '$infoDN' mS-DS-ConsistencyGuid from '$infoCurrentGuid' to '$infoExpectedGuid'" Try { If ($ActiveDirectoryCredential) { Set-ADObject -Identity $infoDN ` -Replace @{'mS-DS-ConsistencyGuid'=$DuplicateUserSourceAnchorInfo.ExpectedMsDsConsistencyGuid} ` -Credential $ActiveDirectoryCredential ` -Server $DuplicateUserSourceAnchorInfo.ADDomainName -ErrorAction Stop } Else { Set-ADObject -Identity $infoDN ` -Replace @{'mS-DS-ConsistencyGuid'=$DuplicateUserSourceAnchorInfo.ExpectedMsDsConsistencyGuid} ` -Server $DuplicateUserSourceAnchorInfo.ADDomainName -ErrorAction Stop } } Catch { Write-Error "Error updating '$($DuplicateUserSourceAnchorInfo.DistinguishedName)' mS-DS-ConsistencyGuid in Active Directory. Error Details: $($_.Exception.Message)" } } ElseIf ($decision -eq 2) { $abort = $true } } } End { If ($abort) { Write-Host "Set-ADSyncToolsDuplicateUsersSourceAnchor execution cancelled." } Else { Write-Host "Set-ADSyncToolsDuplicateUsersSourceAnchor execution complete. Run a sync cycle with 'Start-ADSyncSyncCycle' to clear DuplicateUsers SourceAnchor errors." -ForegroundColor Green } Write-Verbose "Exiting: Set-ADSyncToolsDuplicateUsersSourceAnchor" } } #endregion #======================================================================================= #======================================================================================= #region DirSyncOverrides feature #======================================================================================= <# .Synopsis Compares between on-premises AD and Azure AD the users with DirSyncOverrides set on Mobile and/or OtherMobile .DESCRIPTION Compares all synchronized user objects where 'Mobile' or 'otherMobile' property values differ between Active Directory and Azure AD. Reads all synced user objects from Azure AD. Gets the shadow property values of 'mobile' and/or 'otherMobile' (AlternateMobilePhones). Reads the 'mobile' or 'otherMobile' set in Azure AD for the same objects to compare. Exports the user objects and values that differ between on-premises Active Directory and Azure AD to a CSV file. .EXAMPLE Compare-ADSyncToolsDirSyncOverrides -Credential $(Get-Credential) .OUTPUTS This cmdlet generates a CSV file containing all synchronized user objects which 'mobile' and/or 'otherMobile' (AlternateMobilePhones) values differ between Active Directory and Azure AD. Such objects have a property called "DirSyncOverrides" set which is not publicly visible. Objects with this property set will ignore updates of 'mobile' or 'otherMobile' attributes synchronized from Active Directory to Azure AD. #> Function Compare-ADSyncToolsDirSyncOverrides { [CmdletBinding()] Param ( # Azure AD Global Admin Credential [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [PSCredential] $Credential ) # Verify if MSOnline module is installed and install it if missing $MSOnlineModule = Get-Module -Name MSOnline -ListAvailable If ($null -eq $MSOnlineModule) { InstallModuleDepedency -ModuleName MSOnline } # Connect to Azure AD using the MSOnline module Write-Host "Connecting to Azure AD. Please wait..." -ForegroundColor Cyan Try { Import-Module MSOnline Connect-MsolService -Credential $Credential -ErrorAction Stop } Catch { Throw "An error occurred connecting to Azure AD. Error Details: $($_.Exception.Message)" } # Create file before processing data $filename = Get-ADsyncToolsLogFilename -Name 'DirSyncOverrides' -Extension 'csv' Try { Set-Content $filename "" -ErrorAction Stop } Catch { Throw "An error occurred saving the file '$filename'. Error Details: $($_.Exception.Message)" } $properties = @('SourceAnchor','DisplayName','Mail','Mobile','otherMobile', 'UserPrincipalName') $dirsyncObjs = Get-ADSyncToolsAadObject -SyncObjectType 'User' -Credential $Credential -Properties $properties | Select-Object 'ObjectId','SourceAnchor','DisplayName','Mail','Mobile','otherMobile','UserPrincipalName' Write-Host "Reading all synchronized User objects from Azure AD..." -ForegroundColor Cyan $aadUsers = [System.Collections.ArrayList]@() Get-MsolUser -Synchronized -All | & { process { $null = $aadUsers.Add($($_ | Select-Object ObjectId, MobilePhone, AlternateMobilePhones, LastDirSyncTime)) } } # From the first list get the properties set in Azure AD and compare. # List objects where there are differences of values on Mobile and otherMobile between Active Directory and Azure Active Directory $results = [System.Collections.ArrayList]@() Write-Host "Comparing Mobile and OtherMobile values, this process can take some time. Please wait..." -ForegroundColor Cyan $dirsyncObjsTotalCount = $dirsyncObjs.count $dirsyncObjsCounter = 0 $showProgressTimer = [System.Diagnostics.Stopwatch]::StartNew() ForEach ($dirsyncObj in $dirsyncObjs) { # Show progress every 3s $dirsyncObjsCounter++ If ($showProgressTimer.Elapsed.TotalMilliseconds -ge 3000) { $dirsyncObjsComplete = [math]::Round(($dirsyncObjsCounter/$dirsyncObjsTotalCount*100)) Write-Progress -Activity "Comparing Mobile and OtherMobile values" -Status "$dirsyncObjsComplete% Complete:" -PercentComplete $dirsyncObjsComplete $showProgressTimer.Reset() $showProgressTimer.Start() } # Get user object from Azure AD $aadUser = $aadUsers | where ObjectId -eq $dirsyncObj.ObjectId # Compare synchronized User's DirSync Shadow properties with Azure AD properties If (![string]::IsNullOrEmpty($aadUser.LastDirSyncTime)) { # Compare Mobile and MobilePhone $differentMobile = $false If ([string]::IsNullOrEmpty($dirsyncObj.Mobile) -xor [string]::IsNullOrEmpty($aadUser.MobilePhone)) { # Either Mobile or MobilePhone is empty $differentMobile = $true } # Mobile and MobilePhone are either both empty or both present ElseIf (![string]::IsNullOrEmpty($aadUser.MobilePhone)) { # Both Mobile and MobilePhone are present - Compare Mobile/MobilePhone $dirsyncObjMobile = $null $dirsyncObjMobile = $dirsyncObj.Mobile $aadUserMobile = $null $aadUserMobile = $aadUser.MobilePhone If ($dirsyncObjMobile -ne $aadUserMobile) { $differentMobile = $true } Write-Verbose "Compared AD:Mobile '$dirsyncObjMobile' with AAD:MobilePhone '$aadUserMobile' for object $($dirsyncObj.ObjectId)' | Different = $differentMobile" } # Compare otherMobile and AlternateMobilePhones $differentOtherMobile = $false # Note: $dirsyncObj.otherMobile might be an empty array if previously provisioned and then cleared, or an empty string if never provisionined before If (($dirsyncObj.otherMobile.Count -eq 0 -or $dirsyncObj.otherMobile -eq '') -xor $aadUser.AlternateMobilePhones.Count -eq 0) { # Either otherMobile or AlternateMobilePhones is empty and the other has a value Write-Verbose "Either otherMobile or AlternateMobilePhones is empty and the other has a value" $differentOtherMobile = $true } # Check if otherMobile and AlternateMobilePhones are either both empty or both present ElseIf($aadUser.AlternateMobilePhones.Count -gt 0) { # Both otherMobile and AlternateMobilePhones have values - Compare array size Write-Verbose "Both otherMobile and AlternateMobilePhones have values - Compare array size" If ($dirsyncObj.otherMobile.Count -ne $aadUser.AlternateMobilePhones.Count) { Write-Verbose "Both otherMobile and AlternateMobilePhones entry count is different" $differentOtherMobile = $true } Else { # Compare otherMobile and AlternateMobilePhones values # Note: Not using Compare-Object because it doesn't respect array order. Write-Verbose "Compare otherMobile and AlternateMobilePhones values" For ($i = 0; $i -lt $aadUser.AlternateMobilePhones.Count; $i++) { If ($aadUser.AlternateMobilePhones[$i] -ne $dirsyncObj.otherMobile[$i]) { $differentOtherMobile = $true break } } } Write-Verbose "Compared otherMobile with AlternateMobilePhones for object $($dirsyncObj.ObjectId)' | Different = $differentOtherMobile" } If ($differentOtherMobile -or $differentMobile) { $dirsyncObj | Add-Member NoteProperty "MobileInAAD" $aadUser.MobilePhone -Force $dirsyncObj | Add-Member NoteProperty "OtherMobileInAAD" $aadUser.AlternateMobilePhones -Force $null = $results.Add($dirsyncObj) Write-Verbose "Added object '$($dirsyncObj.ObjectId)' | Different Mobile = $differentMobile | Different OtherMobile = $differentOtherMobile" } } } #END If ($results.count -gt 0) { # Save resulting file $valueDelimiter = ";" Try { $output = $results | Select-Object UserPrincipalName,DisplayName,ObjectId,SourceAnchor,Mobile,MobileInAAD,@{ n='otherMobile';e={($_ | Select-Object -ExpandProperty otherMobile) -join $valueDelimiter }},@{ n='OtherMobileInAAD';e={($_ | Select-Object -ExpandProperty OtherMobileInAAD) -join $valueDelimiter }} $output | Export-Csv -Path $filename -NoTypeInformation -Delimiter ',' -ErrorAction Stop Write-Host "User list exported to '$filename' successfully." -ForegroundColor Green } Catch { Throw "An error occurred saving the file '$filename'. Error Details: $($_.Exception.Message)" } } Else { Write-Host "No users found differing Mobile or OtherMobile (AlternateMobilePhones) values between on-premises Active Directory and Azure AD." -ForegroundColor Cyan } } <# .Synopsis Gets the on-premises AD Mobile and OtherMobile and/or Azure AD MobilePhone and AlternateMobilePhones attributes .DESCRIPTION Returns the value in Mobile/OtherMobile and/or MobilePhone/AlternateMobilePhones from the source directory. Supports Active Directory objects in multi-domain forests. .EXAMPLE Get-ADSyncToolsDirSyncOverridesUser -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' -FromAD .EXAMPLE Get-ADSyncToolsDirSyncOverridesUser -Identity 'User1@Contoso.com' -FromAD .EXAMPLE Get-ADSyncToolsDirSyncOverridesUser 'User1@Contoso.com' -FromAzureAD .EXAMPLE 'User1@Contoso.com' | Get-ADSyncToolsDirSyncOverridesUser -FromAD -FromAzureAD #> Function Get-ADSyncToolsDirSyncOverridesUser { [CmdletBinding()] Param ( # Target object in AD to get [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity, # Get from AD [Parameter(Mandatory=$false, Position=1)] [switch] $FromAD, # Get from AAD [Parameter(Mandatory=$false, Position=2)] [switch] $FromAzureAD ) If (!$FromAD -and !$FromAzureAD) { Throw "Please provide the source directory to get the user object with [-FromAD] and/or [-FromAzureAD] parameters" } If ($FromAD) { # Get object from AD $adObject = Search-ADSyncToolsADobject $Identity -Properties 'Mobile','OtherMobile' Add-Member -InputObject $adObject -MemberType NoteProperty -Name _SourceDirectory -Value "ActiveDirectory" -Force $sortedProps = Get-Member -InputObject $adObject -MemberType NoteProperty | select -ExpandProperty Name | Sort-Object $adObject | select $sortedProps } If ($FromAzureAD) { If (!($Identity -match $upnRegex)) { Write-Warning "Please provide a valid UserPrincipalName to get user from Azure AD." } Else { Try { # Get object from Azure AD $aadObject = Get-MsolUser -UserPrincipalName $Identity -ErrorAction Stop | select $defaultMsolObjProperties } Catch { Throw "Unable to get user '$Identity' from Azure AD. Error Details: $($_.Exception.Message)" } Add-Member -InputObject $aadObject -MemberType NoteProperty -Name _SourceDirectory -Value "AzureAD" -Force $sortedProps = Get-Member -InputObject $aadObject -MemberType NoteProperty | select -ExpandProperty Name | Sort-Object $aadObject | select $sortedProps } } } <# .Synopsis Sets the on-premises AD Mobile and OtherMobile and/or Azure AD MobilePhone and AlternateMobilePhones attributes .DESCRIPTION Updates the value in Mobile/OtherMobile and/or MobilePhone/AlternateMobilePhones in the target directory. Supports Active Directory objects in multi-domain forests. .EXAMPLE Set-ADSyncToolsDirSyncOverridesUser -Identity 'User1@Contoso.com' -MobileInAD '999888777' .EXAMPLE Set-ADSyncToolsDirSyncOverridesUser -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' -MobileInAD '999888777' -OtherMobileInAD '0987654','1234567' .EXAMPLE Set-ADSyncToolsDirSyncOverridesUser -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' -MobileInAD '999888777' -Credential <Domain Credential> .EXAMPLE Set-ADSyncToolsDirSyncOverridesUser 'User1@Contoso.com' -MobilePhoneInAAD '999888777' .EXAMPLE Set-ADSyncToolsDirSyncOverridesUser 'User1@Contoso.com' -AlternateMobilePhonesInAAD '0987654','1234567' .EXAMPLE Set-ADSyncToolsDirSyncOverridesUser 'User1@Contoso.com' -MobileInAD '999888777' -AlternateMobilePhonesInAAD '0987654','1234567' #> Function Set-ADSyncToolsDirSyncOverridesUser { [CmdletBinding()] Param ( # Target object in AD to upodate [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity, # Value to set Mobile in AD [Parameter(Mandatory=$false, Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string] $MobileInAD, # Value to set OtherMobile in AD [Parameter(Mandatory=$false, Position=2, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string[]] $OtherMobileInAD, # Value to set MobilePhone in Azure AD [Parameter(Mandatory=$false, Position=3, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string] $MobilePhoneInAAD, # Value to set AlternateMobilePhones in Azure AD [Parameter(Mandatory=$false, Position=4, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [string[]] $AlternateMobilePhonesInAAD, # Credential for target AD Domain [Parameter(Mandatory=$false, Position=5)] [pscredential] $Credential ) If (![string]::IsNullOrEmpty($MobileInAD) -or ![string]::IsNullOrEmpty($OtherMobileInAD)) { # Get the target object from AD $adObject = Search-ADSyncToolsADobject -Identity $Identity -Properties 'Mobile','OtherMobile' Write-Verbose "Found Object '$($adObject.Name)' | Mobile: $($adObject.Mobile) | OtherMobile: $($adObject.OtherMobile)" If (![string]::IsNullOrEmpty($MobileInAD)) { $targetAttribute = 'Mobile' $targetValue = $MobileInAD # Set new value on target object in AD Set-ADSyncToolsADobject -ADObject $adObject -AttributeName $targetAttribute -AttributeValue $targetValue -Credential $Credential } If ($null -ne $OtherMobileInAD) { $targetAttribute = 'OtherMobile' $targetValue = $OtherMobileInAD # Set new value on target object in AD Set-ADSyncToolsADobject -ADObject $adObject -AttributeName $targetAttribute -AttributeValue $targetValue -Credential $Credential } } If ((![string]::IsNullOrEmpty($MobilePhoneInAAD) -or ![string]::IsNullOrEmpty($AlternateMobilePhonesInAAD)) -and !($Identity -match $upnRegex)) { Throw "Please provide a valid UserPrincipalName to update user in Azure AD." } If (![string]::IsNullOrEmpty($MobilePhoneInAAD)) { Try { Set-MsolUser -UserPrincipalName $Identity -MobilePhone $MobilePhoneInAAD -ErrorAction Stop Write-Verbose "Attribute 'MobilePhone' updated to '$MobilePhoneInAAD' in '$Identity' object successfully" } Catch { Throw "Unable to update 'MobilePhone' with '$OtherMobileInAD' in '$Identity' object: $($_.Exception.Message)" } } If (![string]::IsNullOrEmpty($AlternateMobilePhonesInAAD)) { Try { Set-MsolUser -UserPrincipalName $Identity -AlternateMobilePhones $AlternateMobilePhonesInAAD -ErrorAction Stop Write-Verbose "Attribute 'AlternateMobilePhones' updated to '$AlternateMobilePhonesInAAD' in '$Identity' object successfully" } Catch { Throw "Unable to update 'AlternateMobilePhones' with '$AlternateMobilePhonesInAAD' in '$Identity' object: $($_.Exception.Message)" } } } <# .Synopsis Clears the on-premises AD Mobile and OtherMobile and/or Azure AD MobilePhone and AlternateMobilePhones attributes .DESCRIPTION Updates the value in Mobile/OtherMobile and/or MobilePhone/AlternateMobilePhones in the target directory. Supports Active Directory objects in multi-domain forests. .EXAMPLE Clear-ADSyncToolsDirSyncOverridesUser -Identity 'User1@Contoso.com' -MobileInAD .EXAMPLE Clear-ADSyncToolsDirSyncOverridesUser -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' -MobileInAD -OtherMobileInAD .EXAMPLE Clear-ADSyncToolsDirSyncOverridesUser -Identity 'CN=User1,OU=Sync,DC=Contoso,DC=com' -MobileInAD -Credential <Domain Credential> .EXAMPLE Clear-ADSyncToolsDirSyncOverridesUser 'User1@Contoso.com' -MobilePhoneInAAD .EXAMPLE Clear-ADSyncToolsDirSyncOverridesUser 'User1@Contoso.com' -MobilePhoneInAAD -AlternateMobilePhonesInAAD #> Function Clear-ADSyncToolsDirSyncOverridesUser { [CmdletBinding()] Param ( # Target object in AD to upodate [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity, # Value to set Mobile in AD [Parameter(Mandatory=$false, Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [switch] $MobileInAD, # Value to set OtherMobile in AD [Parameter(Mandatory=$false, Position=2, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [switch] $OtherMobileInAD, # Value to set MobilePhone in Azure AD [Parameter(Mandatory=$false, Position=3, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [switch] $MobilePhoneInAAD, # Value to set AlternateMobilePhones in Azure AD [Parameter(Mandatory=$false, Position=4, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [switch] $AlternateMobilePhonesInAAD, # Credential for target AD Domain [Parameter(Mandatory=$false, Position=5)] [pscredential] $Credential ) If ($MobileInAD -or $OtherMobileInAD) { # Get the target object from AD $adObject = Search-ADSyncToolsADobject -Identity $Identity -Properties 'Mobile','OtherMobile' Write-Verbose "Found Object '$($adObject.Name)' | Mobile: $($adObject.Mobile) | OtherMobile: $($adObject.OtherMobile)" If ($MobileInAD) { $targetAttribute = 'Mobile' # Set new value on target object in AD Clear-ADSyncToolsADobject -ADObject $adObject -AttributeName $targetAttribute -Server $targetDC -Credential $Credential } If ($OtherMobileInAD) { $targetAttribute = 'OtherMobile' # Set new value on target object in AD Clear-ADSyncToolsADobject -ADObject $adObject -AttributeName $targetAttribute -Server $targetDC -Credential $Credential } } If (($MobilePhoneInAAD -or $AlternateMobilePhonesInAAD) -and !($Identity -match $upnRegex)) { Throw "Please provide a valid UserPrincipalName to update user in Azure AD." } If ($MobilePhoneInAAD) { Try { Set-MsolUser -UserPrincipalName $Identity -MobilePhone "" -ErrorAction Stop Write-Verbose "Attribute 'MobilePhone' cleared in '$Identity' object successfully" } Catch { Throw "Unable to clear 'MobilePhone' in '$Identity' object: $($_.Exception.Message)" } } If ($AlternateMobilePhonesInAAD) { Try { Set-MsolUser -UserPrincipalName $Identity -AlternateMobilePhones @() -ErrorAction Stop Write-Verbose "Attribute 'AlternateMobilePhones' cleared in '$Identity' object successfully" } Catch { Throw "Unable to clear 'AlternateMobilePhones' in '$Identity' object: $($_.Exception.Message)" } } } #endregion #======================================================================================= #======================================================================================= #region Microsoft Graph - Onpremises Attributes #======================================================================================= <# .SYNOPSIS Converts Key/Value pairs into Json Body for Graph parameters Convers null values to a non-string 'null' #> Function ConvertTo-ADSyncToolsGraphJsonBody { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] $NameValuePair ) Write-Verbose "ConvertTo-ADSyncToolsGraphJsonBody: input value type = $($NameValuePair.GetType())" $h = @{} # Convert Key/Value pairs into Hashtable foreach ($nvp in $NameValuePair) { if ([string]::IsNullOrEmpty($nvp.Value)) { $h[$nvp.Key] = 'nullString' } else { $h[$nvp.Key] = $nvp.Value } } # Convert Hashtable to Json Body $hJson = $h | ConvertTo-Json # return Json Body converting null values return $($hJson.Replace('"nullString"', "null")) } <# .SYNOPSIS Invoke Microsoft Graph call #> function Get-ADSyncToolsGraphInvoke { [CmdletBinding()] param ( # URI string [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [string] $Uri, # Filter string [Parameter(Mandatory=$false, Position=1)] [string] $Filter, # Headers string [Parameter(Mandatory=$false, Position=2)] [hashtable] $Headers, # Properties to select [Parameter(Mandatory=$false, Position=3)] [string] $Property ) #begin if (-not [string]::IsNullOrEmpty($Filter)) { $Uri += '?$filter=' + $Filter } if (-not [string]::IsNullOrEmpty($Property)) { If ($Uri.Contains('?')) { $Uri += '&$select=' + $Property } else { $Uri += '?$select=' + $Property } } Write-Verbose "GET $Uri" #process do { if ($null -eq $Headers) { $response = Invoke-MgGraphRequest GET $Uri -OutputType psobject } else { $response = Invoke-MgGraphRequest GET $Uri -OutputType psobject -Headers $Headers } $Uri = $response.'@odata.nextLink' if ($response.PSobject.Properties.name -match 'value') { $response.value } else { $response | Select-Object $($Property -split ',') } } while ($Uri) #end } <# .SYNOPSIS Get one user or all users containing onpremises properties in Entra ID .DESCRIPTION This function can be used to get the onpremises attributes listed below from all users or a specific user in Entra ID: onPremisesDistinguishedName onPremisesDomainName onPremisesImmutableId onPremisesSamAccountName onPremisesSecurityIdentifier onPremisesUserPrincipalName Note: It only returns the users that have onpremises attributes populated. By Default it returns all cloud-only users, but you can specify -IncludeSyncedUsers to return all users, including users synced from on-premises AD. Requires Microsoft Graph PowerShell SDK, authenticated with: Connect-MgGraph -Scopes "User.Read.All" .EXAMPLE Get the onpremises attributes of all cloud-users that have onpremises attributes populated. Get-ADSyncToolsOnPremisesAttribute .EXAMPLE Get the onpremises attributes of all users that have onpremises attributes populated. Get-ADSyncToolsOnPremisesAttribute -IncludeSyncedUsers .EXAMPLE Get the onpremises attributes for one specific user (verbose) Get-ADSyncToolsOnPremisesAttribute -Id '2b3e5a05-6f08-40b2-8b66-233430d395a2' -Verbose .EXAMPLE Get the onpremises attributes for one specific user (pipelining) 'User1@Contoso.com' | Get-ADSyncToolsOnPremisesAttribute .EXAMPLE Get only specific onpremises attributes of a user (pipelining) 'User1@Contoso.com' | Get-ADSyncToolsOnPremisesAttribute -Property @('onPremisesSyncEnabled','onPremisesImmutableId') .EXAMPLE Export onpremises attributes of all the users Get-ADSyncToolsOnPremisesAttribute | Export-Csv backupOnpremisesAttributes.csv -Delimiter ';' #> function Get-ADSyncToolsOnPremisesAttribute { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'SingleUser', ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [Alias("Identity")] [string] $Id, [Parameter(Mandatory = $false, ParameterSetName = 'AllUsers', ValueFromPipelineByPropertyName=$true, Position=0)] [switch] $IncludeSyncedUsers, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName=$true, Position=1)] [string[]] $Property ) begin { Write-Verbose "ParameterSetName: $($PSCmdlet.ParameterSetName)" Write-Verbose "Id: $Id" Write-Verbose "IncludeSyncedUsers: $IncludeSyncedUsers" Write-Verbose "Property: $Property" Confirm-ADSyncToolsGraphConnection -RequiredScope "User.Read.All" $baseUriV1 = 'https://graph.microsoft.com/v1.0/users/' # Properties to include in $select if ($Property) { # Always force output of 'id' in case there's no other valid properties $mandatoryAttributes = @('onPremisesSyncEnabled','userPrincipalName','id') $onpremPropertiesList = $mandatoryAttributes + $Property | Select-Object -Unique | Sort-Object $onpremProperties = $onpremPropertiesList -join ',' } else { [string[]] $onpremPropertyList = @('id','userPrincipalName','onPremisesSyncEnabled') $onpremPropertyList += $defaultGraphOnPremisesProperties $onpremProperties = ($onpremPropertyList | Select-Object -Unique) -join ',' } Write-Verbose "Properties to get: `$select=$onpremProperties" } process { if ($PSCmdlet.ParameterSetName -eq 'SingleUser') { # Get a specific user Write-Verbose "Get single User: $Id" Get-ADSyncToolsGraphInvoke -Uri "$baseUriV1$Id" -Property $onpremProperties } elseif ($PSCmdlet.ParameterSetName -eq 'AllUsers') { # Get All users Write-Verbose "Get all Users" # Headers to prevent: Filter operator 'NotEqualsMatch' is not supported. $headers = @{'ConsistencyLevel'= 'eventual'} # Query Filter for users with OnPremises properties # NOTE: Can only use OnPremisesDistinguishedName and onPremisesSecurityIdentifier with conditional operators (or/and), # other properties return "An unsupported property was specified" error. If ($IncludeSyncedUsers) { # All users with onPremisesDistinguishedName, including synced users - $count is required for NotEqualsMatch $filter = 'OnPremisesDistinguishedName ne null or onPremisesSecurityIdentifier ne null&$count=true' } else { # Cloud-only users with onPremisesDistinguishedName - $count is required for NotEqualsMatch $filter = '(OnPremisesDistinguishedName ne null or onPremisesSecurityIdentifier ne null) and onPremisesSyncEnabled ne true&$count=true' } Write-Verbose "Query filter: $filter" Get-ADSyncToolsGraphInvoke -Uri $baseUriV1 -Filter $filter -Headers $headers -Property $onpremProperties } } end {} } <# .SYNOPSIS Set onpremises attributes for a cloud user in Entra ID .DESCRIPTION .DESCRIPTION This function can be used to set any of the OnPremises attributes listed below: onPremisesDistinguishedName onPremisesDomainName onPremisesImmutableId onPremisesSamAccountName onPremisesSecurityIdentifier * onPremisesUserPrincipalName It also supports clearing an attribute if an empty string "" is specified (see examples). Requires Microsoft Graph PowerShell SDK, authenticated with: Connect-MgGraph -Scopes "User.ReadWrite.All" * Must have the correct Security Identifier format, e.g.: "S-1-5-21-4097605469-3104078553-1111111111-1111" .EXAMPLE Set only onPremisesImmutableId (pipelining) 'User1@Contoso.com' | Set-ADSyncToolsOnPremisesAttribute -onPremisesImmutableId 'nofCJe0gZk6D8J4gRgrt+A==' .EXAMPLE Set onPremisesSamAccountName and clear onPremisesImmutableId Set-ADSyncToolsOnPremisesAttribute 'User1@Contoso.com' -onPremisesSamAccountName 'User1' ` -onPremisesImmutableId "" .EXAMPLE Set each onpremises attributes explicitly Set-ADSyncToolsOnPremisesAttribute 'User1@Contoso.com' -onPremisesUserPrincipalName "User1@Contoso.com" ` -onPremisesDistinguishedName "CN=User1,OU=Sync,DC=Contoso,DC=com" ` -onPremisesDomainName 'Contoso.com' ` -onPremisesImmutableId 'nofCJe0gZk6D8J4gRgrt+A==' ` -onPremisesSamAccountName 'User1' ` -onPremisesSecurityIdentifier "S-1-5-21-4097605469-3104078553-1111111111-1111" .EXAMPLE Set onpremises attributes based on a json parameter body (-BodyParameter) $jsonBody = @' { "onPremisesDistinguishedName": "User1@Contoso.com", "onPremisesDomainName": 'Contoso.com', "onPremisesImmutableId": 'nofCJe0gZk6D8J4gRgrt+A==', "onPremisesSamAccountName": 'User1', "onPremisesSecurityIdentifier": "S-1-5-21-4097605469-3104078553-1111111111-1111", "onPremisesUserPrincipalName": "User1@Contoso.com" } '@ Set-ADSyncToolsOnPremisesAttribute -Identity '98765432-6f08-40b2-8b66-123456789012' -BodyParameter $jsonBody #> function Set-ADSyncToolsOnPremisesAttribute { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [Alias("Identity")] [string] $Id, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=0)] [string] $onPremisesDistinguishedName, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=1)] [string] $onPremisesDomainName, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=2)] [string] $onPremisesImmutableId, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=3)] [string] $onPremisesSamAccountName, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=4)] [string] $onPremisesSecurityIdentifier, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=5)] [string] $onPremisesUserPrincipalName, [Parameter(Mandatory = $true, ParameterSetName = 'ByBodyParameter', ValueFromPipelineByPropertyName=$true, Position=0)] [string] $BodyParameter ) begin { Write-Verbose "ParameterSetName: $($PSCmdlet.ParameterSetName)" Write-Verbose "Id: $Id" Confirm-ADSyncToolsGraphConnection -RequiredScope "User.ReadWrite.All" # Set target endpoint/resource $baseUriBeta = "https://graph.microsoft.com/beta/users/" } process { if ($PSCmdlet.ParameterSetName -eq 'ByProperty') { Write-Verbose "Set by property: $Id" # get input parameters $inputParameters = $PSBoundParameters $inputOnPremisesParam = @($inputParameters.GetEnumerator() | Where-Object {$_.Key -like 'onPremises*'}) Write-Verbose "InputParameterOnPremises: $($inputOnPremisesParam -join ',') | Count=$($inputOnPremisesParam.Count)" if ($inputOnPremisesParam.Count -gt 0) { # Convert input values to json body parameter Write-Verbose "Converting (attribute, value) to Json: $($inputOnPremisesParam)" [string] $BodyParameter = ConvertTo-ADSyncToolsGraphJsonBody ($inputOnPremisesParam) Write-Verbose "`n$BodyParameter" } else { Throw "Invalid function parameters: Missing a BodyParameter or an attribute name parameter." } } elseif ($PSCmdlet.ParameterSetName -eq 'ByBodyParameter') { Write-Verbose "Set by BodyParameter: $Id" } Invoke-MgGraphRequest -uri $baseUriBeta$Id -Body $BodyParameter -Method PATCH } end {} } <# .SYNOPSIS Clear onpremises attributes on a cloud user in Entra ID .DESCRIPTION This function can be used to clear any of the OnPremises attributes listed below: onPremisesDistinguishedName onPremisesDomainName onPremisesSamAccountName onPremisesSecurityIdentifier onPremisesUserPrincipalName onPremisesImmutableId * For safety reasons the onPremisesImmutableId will not be included in -All parameter because this attribute was never cleared as part of DirSync disablement. Keeping onPremisesImmutableId populated is not harmful and can allow you to hard-match an existent on-premises AD user with the Entra ID user. If you also want to clear the onPremisesImmutableId use the -BodyParameter option or run Clear-ADSyncToolsOnPremisesAttribute with '-onPremisesImmutableId' parameter instead of -All. Requires Microsoft Graph PowerShell SDK, authenticated with: Connect-MgGraph -Scopes "User.ReadWrite.All" .EXAMPLE Clear all onpremises attributes for one user (pipelining) 'User1@Contoso.com' | Clear-ADSyncToolsOnPremisesAttribute -All .EXAMPLE Clear all onpremises attributes for all users (pipelining and verbose) - with a backup to CSV first Get-ADSyncToolsOnPremisesAttribute | Export-Csv backupOnpremisesAttributes.csv -Delimiter ';' Get-ADSyncToolsOnPremisesAttribute | Select-Object id | Clear-ADSyncToolsOnPremisesAttribute -All -Verbose .EXAMPLE Clear only onPremisesImmutableId attribute Clear-ADSyncToolsOnPremisesAttribute -Identity '98765432-6f08-40b2-8b66-123456789012' -onPremisesImmutableId .EXAMPLE Clear all onpremises attributes explicitly Clear-ADSyncToolsOnPremisesAttribute 'User1@Contoso.com' -onPremisesDistinguishedName -onPremisesDomainName ` -onPremisesImmutableId -onPremisesSamAccountName ` -onPremisesSecurityIdentifier -onPremisesUserPrincipalName .EXAMPLE Clear all onpremises attributes based on a json parameter body (-BodyParameter) $jsonBody = @' { "onPremisesDistinguishedName": null, "onPremisesDomainName": null, "onPremisesImmutableId": null, "onPremisesSamAccountName": null, "onPremisesSecurityIdentifier": null, "onPremisesUserPrincipalName": null } '@ Clear-ADSyncToolsOnPremisesAttribute -Identity $userId -BodyParameter $jsonBody #> function Clear-ADSyncToolsOnPremisesAttribute { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [Alias("Identity")] [string] $Id, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=0)] [switch] $onPremisesDistinguishedName, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=1)] [switch] $onPremisesDomainName, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=2)] [switch] $onPremisesImmutableId, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=3)] [switch] $onPremisesSamAccountName, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=4)] [switch] $onPremisesSecurityIdentifier, [Parameter(Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName=$true, Position=5)] [switch] $onPremisesUserPrincipalName, [Parameter(Mandatory = $true, ParameterSetName = 'ByBodyParameter', ValueFromPipelineByPropertyName=$true, Position=0)] [string] $BodyParameter, [Parameter(Mandatory = $true, ParameterSetName = 'ClearAll', ValueFromPipelineByPropertyName=$true, Position=0)] [switch] $All ) begin { Write-Verbose "ParameterSetName: $($PSCmdlet.ParameterSetName)" Write-Verbose "Id: $Id" Confirm-ADSyncToolsGraphConnection -RequiredScope "User.ReadWrite.All" # Set target endpoint/resource $baseUriBeta = "https://graph.microsoft.com/beta/users/" } process { if ($PSCmdlet.ParameterSetName -eq 'ByProperty') { Write-Verbose "Set by property: $Id" # get input parameters $inputParameters = $PSBoundParameters $inputOnPremisesParam = @($inputParameters.GetEnumerator() | Where-Object {$_.Key -like 'onPremises*'}) Write-Verbose "InputParameterOnPremises: $($inputOnPremisesParam -join ',') | Count=$($inputOnPremisesParam.Count)" if ($inputOnPremisesParam.Count -gt 0) { # Convert input parameters to Hashtable $ht = New-Object System.Collections.Hashtable $inputOnPremisesParam | ForEach-Object {$ht.Add($_.Key, "nullString")} # Convert Hashtable to Json Body $htJson = $ht | ConvertTo-Json # create Json Body placing null strings $body = $htJson.Replace('"nullString"', "null") } else { Throw "Invalid function parameters: Missing a BodyParameter or an attribute name parameter." } } elseif ($PSCmdlet.ParameterSetName -eq 'ByBodyParameter') { Write-Verbose "Clear by BodyParameter: $Id" $body = $BodyParameter } elseif ($PSCmdlet.ParameterSetName -eq 'ClearAll') { Write-Verbose "Clear All Onpremises attributes: $Id" $body = @' { "onPremisesDistinguishedName": null, "onPremisesDomainName": null, "onPremisesSamAccountName": null, "onPremisesSecurityIdentifier": null, "onPremisesUserPrincipalName": null } '@ } Invoke-MgGraphRequest -uri $baseUriBeta$Id -Body $body -Method PATCH } end {} } #endregion #======================================================================================= #======================================================================================= #region Internal Notes #======================================================================================= <# #TODO: Review naming convention of output files: ADSyncTools-SyncTrace_20210812-225734.log << correct format LdapTrace_20210811200546-attributeTypes.txt ADimportTrace_20210811222443.log 20210812223506_ADSyncAADHybridJoinCertificateReport.csv TODO: Use -Credential $ActiveDirectoryCredential in Search AD object, Get/Set AD CG Support queries to parent domains from child domains # Target Domain Credential [Parameter(Mandatory=$false, Position=1)] [ValidateNotNullOrEmpty()] $Credential, # Target Domain Server [Parameter(Mandatory=$false, Position=2)] [ValidateNotNullOrEmpty()] $Server #> #endregion #======================================================================================= #======================================================================================= #region NetApi32 Init #======================================================================================= Add-Type -TypeDefinition @" using System; using System.Diagnostics; using System.Runtime.InteropServices; public static class NetApi32 { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct DomainControllerInfo { public string DomainControllerName; public string DomainControllerAddress; public int DomainControllerAddressType; public Guid DomainGuid; public string DomainName; public string DnsForestName; public int Flags; public string DcSiteName; public string ClientSiteName; } [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] public struct USER_INFO_1 { public string sUsername; public string sPassword; public uint uiPasswordAge; public uint uiPriv; public string sHome_Dir; public string sComment; public uint uiFlags; public string sScript_Path; } //uiPriv public const uint USER_PRIV_GUEST = 0; public const uint USER_PRIV_USER = 1; public const uint USER_PRIV_ADMIN = 2; //uiFlags (flags) public const uint UF_PASSWD_CANT_CHANGE = 0x40; public const uint UF_DONT_EXPIRE_PASSWD = 0x10000; public const uint UF_MNS_LOGON_ACCOUNT = 0x20000; public const uint UF_SMARTCARD_REQUIRED = 0x40000; public const uint UF_TRUSTED_FOR_DELEGATION = 0x80000; public const uint UF_NOT_DELEGATED = 0x100000; public const uint UF_USE_DES_KEY_ONLY = 0x200000; public const uint UF_DONT_REQUIRE_PREAUTH = 0x400000; public const uint UF_PASSWORD_EXPIRED = 0x800000; public const uint UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000; public const uint UF_NO_AUTH_DATA_REQUIRED = 0x2000000; public const uint UF_PARTIAL_SECRETS_ACCOUNT = 0x4000000; public const uint UF_USE_AES_KEYS = 0x8000000; //uiFlags (choice) public const uint UF_TEMP_DUPLICATE_ACCOUNT = 0x0100; public const uint UF_NORMAL_ACCOUNT = 0x0200; public const uint UF_INTERDOMAIN_TRUST_ACCOUNT = 0x0800; public const uint UF_WORKSTATION_TRUST_ACCOUNT = 0x1000; public const uint UF_SERVER_TRUST_ACCOUNT = 0x2000; [DllImport("NetApi32.dll", CharSet=CharSet.Unicode)] public static extern int NetUserGetInfo(string servername, string username, int level, out IntPtr buffer); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool LogonUser(string user, string domain, string password, int logonType, int logonProvider, out IntPtr token); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr handle); [DllImport("NetApi32.dll")] public static extern int NetApiBufferFree(IntPtr buffer); [DllImport("Netapi32.dll", CharSet=CharSet.Unicode)] public static extern int DsGetDcName(string ComputerName, string DomainName, int DomainGuid, string SiteName, int Flags, out IntPtr pDOMAIN_CONTROLLER_INFO); } "@ #endregion #======================================================================================= Export-ModuleMember Install-ADSyncToolsPrerequisites, ` # Installs all PowerShell depedencies Connect-ADSyncTools, ` # Connects ADSyncTools Module to Azure AD and Exchange Online Search-ADSyncToolsADobject, ` # Searches for an AD object in Active Directory Forest Set-ADSyncToolsADobject, ` # Sets an object's attribute in Active Directory Forest Get-ADSyncToolsMsDsConsistencyGuid, ` # Gets an AD object ms-ds-ConsistencyGuid Set-ADSyncToolsMsDsConsistencyGuid, ` # Sets an AD object ms-ds-ConsistencyGuid Clear-ADSyncToolsMsDsConsistencyGuid, ` # Clears an AD object mS-DS-ConsistencyGuid ConvertFrom-ADSyncToolsImmutableID, ` # Converts Base64 ImmutableId (SourceAnchor) to GUID value ConvertTo-ADSyncToolsImmutableID, ` # Converts GUID (ObjectGUID / ms-Ds-Consistency-Guid) to a Base64 string ConvertFrom-ADSyncToolsAadDistinguishedName, ` # Converts AAD Connector DistinguishedName to ImmutableId ConvertTo-ADSyncToolsAadDistinguishedName, ` # Converts ImmutableId to AAD Connector DistinguishedName ConvertTo-ADSyncToolsCloudAnchor, ` # Converts Base64 Anchor to CloudAnchor Export-ADSyncToolsAadDisconnectors, ` # Exports Azure AD Disconnector objects Get-ADSyncToolsAadObject, ` # Gets synced objects for a given SyncObjectType Set-ADSyncToolsAadObject, ` # Sets synced objects for a given SyncObjectType Remove-ADSyncToolsAadObject, ` # Removes orphaned object(s) from Azure AD Export-ADSyncToolsAadPublicFolders, ` # Exports all synced Mail-Enabled Public Folder objects from AzureAD to a CSV file Remove-ADSyncToolsAadPublicFolders, ` # Removes orphaned Mail-Enabled Public Folder object(s) from Azure AD Get-ADSyncToolsRunHistory, ` # Gets ADSync Run History Get-ADSyncToolsRunStepHistory, ` # Gets ADSync Run Profile history including each Run Step result Export-ADSyncToolsRunHistory, ` # Exports ADSync Run History Import-ADSyncToolsRunHistory , ` # Imports ADSync Run History Get-ADSyncToolsRunHistoryLegacyWmi, ` # Gets ADSync Run History for older versions of AAD Connect (WMI) Remove-ADSyncToolsExpiredCertificates, ` # Removes Expired Certificates from a users in AD Trace-ADSyncToolsADImport, ` # Generates a trace file with AD Import step data Trace-ADSyncToolsLdapSchemaQuery, ` # Traces LDAP queries against Active Directory Schema Trace-ADSyncToolsLdapQuery, ` # Traces LDAP queries Export-ADSyncToolsHybridAadJoinReport, ` # Generates a report of certificates stored in Active Directory Computer objects Start-ADSyncToolsLogmanTrace, ` # Starts an ETW (Verbose) trace for SyncRulesPipeline debugging Stop-ADSyncToolsLogmanTrace, ` # Stops the ETW (Verbose) trace for SyncRulesPipeline debugging Get-ADSyncToolsLogmanTraceLevel, ` # Gets the current ETW trace level for SyncRulesPipeline debugging Set-ADSyncToolsLogmanTraceLevel, ` # Sets the ETW trace level (Warning/Verbose) for SyncRulesPipeline debugging Convert-ADSyncToolsLogmanTrace, ` # Decodes an ETW trace for SyncRulesPipeline debugging into a CSV text file Resolve-ADSyncToolsLogmanTrace, ` # Decodes an ETW trace for SyncRulesPipeline debugging and translates the ObjectIds to object names Start-ADSyncToolsCustomSyncScheduler, ` # Custom Sync Scheduler with a specific Connector's order Export-ADSyncToolsObjects, ` # Dumps internal ADsync object(s) to XML file(s) Import-ADSyncToolsObjects, ` # Imports internal ADSync object from XML file Export-ADSyncToolsADpermissionsReport, # Exports AD effective/permissions that AD DS Connector Account has over an object Import-ADSyncToolsADpermissionsReport, # Imports AD permissions data from XML file and returns a DACL table Import-ADSyncToolsSourceAnchor, ` # Imports ImmutableId values from Azure AD Export-ADSyncToolsSourceAnchorReport, ` # Exports a list of mS-DS-ConsistencyGuid values to update in local AD Update-ADSyncToolsSourceAnchor, ` # Updates mS-DS-ConsistencyGuid values for users in local AD Get-ADSyncToolsTenantAzureEnvironment, ` # Gets the tenant azure environment Get-ADSyncToolsADconnectorAccount, ` # Gets the current AD DS Connector account(s) configured in Azure AD Connect Get-ADSyncToolsServiceAccount, ` # Gets the current ADSync service account configured for Azure AD Connect Test-ADSyncToolsPasswordWriteback, ` # Test Password Writeback operations in local Active Directory Start-ADSyncToolsSingleObjectSync, ` # Automates troubleshooting with Single Object Sync tool Repair-ADSyncToolsAutoUpgradeState, ` # Fixes AutoUpgrade Suspended state Get-ADSyncToolsTls12, ` # Gets Client\Server TLS 1.2 settings for .NET Framework Set-ADSyncToolsTls12, ` # Sets Client\Server TLS 1.2 settings for .NET Framework Get-ADSyncToolsDuplicateUsersSourceAnchor,` # Gets duplicate user details which contain 'Source Anchor has changed' error Set-ADSyncToolsDuplicateUsersSourceAnchor, ` # Sets correct source anchor(MsDsConsistencyGuid) values for duplicate users which contain 'Source Anchor has changed' error Compare-ADSyncToolsDirSyncOverrides, ` # Compares between on-premises AD and Azure AD the users with DirSyncOverrides set on Mobile and/or OtherMobile Get-ADSyncToolsDirSyncOverridesUser, ` # Gets the on-premises AD Mobile and OtherMobile and/or Azure AD MobilePhone and AlternateMobilePhones attributes Set-ADSyncToolsDirSyncOverridesUser, ` # Sets the on-premises AD Mobile and OtherMobile and/or Azure AD MobilePhone and AlternateMobilePhones attributes Clear-ADSyncToolsDirSyncOverridesUser, ` # Clears the on-premises AD Mobile and OtherMobile and/or Azure AD MobilePhone and AlternateMobilePhones attributes Get-ADSyncToolsOnPremisesAttribute, ` # Gets the on-premises attributes from a user or for all users with on-premises attributes present (e.g., onPremisesDistinguishedName, etc) Set-ADSyncToolsOnPremisesAttribute, ` # Sets the on-premises attributes from a user (e.g., onPremisesDistinguishedName, etc) Clear-ADSyncToolsOnPremisesAttribute, ` # Clears the on-premises attributes from a user (e.g., onPremisesDistinguishedName, onPremisesDomainName, onPremisesImmutableId, etc) New-ADSyncToolsSqlConnection, ` # SQL Diagnostics Connect-ADSyncToolsSqlDatabase, ` # SQL Diagnostics Invoke-ADSyncToolsSqlQuery,` # SQL Diagnostics Resolve-ADSyncToolsSqlHostAddress, ` # SQL Diagnostics Test-ADSyncToolsSqlNetworkPort, ` # SQL Diagnostics Get-ADSyncToolsSqlBrowserInstances, ` # SQL Diagnostics Get-ADSyncToolsSqlProtocols ` # SQL Diagnostics #======================================================================================= #region Main #======================================================================================= Write-Host "`nADSyncTools for Entra Connect Synchronization" -ForegroundColor Cyan Write-Host "To show all available cmdlets, type: Get-Command -Module ADSyncTools" Write-Host "To show more help information, type: Get-Help <cmdlet> -Full`n" Confirm-ADSyncToolsPowerShellV7 Import-ADSyncToolsModule -ModuleName Microsoft.Graph.Authentication -CheckOnly "`n" #endregion #======================================================================================= |