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 � 2022 Microsoft Corporation. All rights reserved. # #------------------------------------------------------------------------------------------------------------------------------------- # # NAME: Azure AD Connect ADSyncTools PowerShell Module # #------------------------------------------------------------------------------------------------------------------------------------- #======================================================================================= #region Internal Variables #======================================================================================= [string[]] $defaultADobjProperties = @('ObjectClass','UserPrincipalName','ObjectGUID','ObjectSID','mS-DS-ConsistencyGuid','sAMAccountName','CanonicalName','msDS-PrincipalName') [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] $netbiosDomainRegex = "w*\\\w+" #endregion #======================================================================================= #======================================================================================= #region class definitions #======================================================================================= class DuplicateUserSourceAnchorInfo { [string] $UserName [string] $DistinguishedName [string] $ADDomainName [Byte[]] $CurrentMsDsConsistencyGuid [Byte[]] $ExpectedMsDsConsistencyGuid } #======================================================================================= #endregion class definitions #======================================================================================= #======================================================================================= #region Internal Functions #======================================================================================= <# .SYNOPSIS Checks if ActiveDirectory PowerShell Module is present and imports it #> Function Import-ADSyncToolsActiveDirectoryModule { [CmdletBinding()] Param () If (-not (Get-Module ActiveDirectory)) { Try { # Load ActiveDirectory module Import-Module ActiveDirectory -ErrorAction Stop } Catch { Throw "Unable to import ActiveDirectory PowerShell Module. Run 'Install-WindowsFeature RSAT-AD-Tools' to install Active Directory RSAT. Error Details: $($_.Exception.Message)" } } } <# .SYNOPSIS Checks if AADConnector PowerShell Module is present and imports it #> Function Import-ADSyncToolsAADConnectorBinaries { [CmdletBinding()] Param () IsAADConnectPresent -MinVersion "1.6" $binariesPath = Get-ADSyncToolsADsyncFolder If ($binariesPath -eq '') { Write-Warning "Azure AD Connect installation was not found, some functionality may be unavailable. Using current directory '$PSScriptRoot'." Try { Add-Type -Path $(Join-Path -Path $PSScriptRoot -ChildPath "Microsoft.MetadirectoryServicesEx.dll") Add-Type -Path $(Join-Path -Path $PSScriptRoot -ChildPath "Microsoft.MetadirectoryServices.PasswordHashSynchronization.Types.dll") Add-Type -Path $(Join-Path -Path $PSScriptRoot -ChildPath "Microsoft.Azure.ActiveDirectory.Connector.dll") } Catch { Throw "Unable to import AADConnector binaries. Error Details: $($_.Exception.Message)" } } Else { 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\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 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 "Azure AD Connect 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 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 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 $domain = Get-ADDomain -Identity $domainDN Write-Verbose "Domain FQDN: $($domain.DNSRoot)" If ($domain -eq $null) { Throw "Cannot find Domain for object '$DistinguishedName'." } Return $domain.DNSRoot } <# .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 ) 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 $defaultADobjProperties -SearchBase $domainDN -SearchScope Subtree -Server $targetDC" Try { $seachResult = Get-ADObject -Filter "distinguishedName -eq '$DistinguishedName'" -Properties $defaultADobjProperties -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 ) Import-ADSyncToolsActiveDirectoryModule $targetDC = Get-ADSyncToolsADtargetDC -Service GlobalCatalog # Get the AD object from target DC Write-Verbose "Executing: Get-ADObject -Filter `"UserPrincipalName -eq '$UserPrincipalName'`" -Server `"$($targetDC):3268`"" Try { $globalCatalogObj = Get-ADObject -Filter "UserPrincipalName -eq '$UserPrincipalName'" -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 } Return $seachResult } #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 in AD to set ConsistencyGuid [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] $Identity ) Import-ADSyncToolsActiveDirectoryModule 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 } 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" $seachResult = Search-ADSyncToolsADobjectByUPN -UserPrincipalName $Identity } 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 } 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 $defaultADobjProperties -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 = "" | Select Name, ObjectClass, DistinguishedName, ObjectGUID, mS-DS-ConsistencyGuid, ObjectSID, sAMAccountName, UserPrincipalName $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 ($seachResult.'mS-DS-ConsistencyGuid' -ne $null) { 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 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 ) # 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 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 ) # 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)" } # PROCESS $results = @() Do { Try { $batch = $enumerator.EnumerateNextBatch() } Catch { Throw "There was a problem with the Enumerator component. Error Details: $($_.Exception.Message)" } If ($batch.AadBatch.ResultObjects.Count -gt 0) { ForEach($entry in $batch.AadBatch.ResultObjects) { # Skip deleted objects If ($entry.SyncOperation -ne 'Delete') { $r = "" | select ObjectClass, ObjectId, CloudAnchor $r.CloudAnchor = $entry.PropertyValues.CloudAnchor $cloudAnchoraSplit = $r.CloudAnchor -split '_' $r.ObjectClass = $cloudAnchoraSplit[0] $r.ObjectId = $cloudAnchoraSplit[1] # Add all non-null properties as a string value $properties = $entry.PropertyValues # Also Supports CloudLegacyExchangeDN an CloudMSExchRecipientDisplayType properties $propertyKeys = @('SourceAnchor','DisplayName','Mail') ForEach ($propKey in $propertyKeys) { $propValue = $properties[$propKey] if ($propValue -ne $null) { $propValue = $propValue.ToString() } Else { $propValue = "" } Add-Member -InputObject $r -MemberType NoteProperty -Name $propKey -Value $propValue -Force } # Add ProxyAddresses as an array $proxyAddresses = @() If ($($properties['ProxyAddresses']).Count -gt 0) { ForEach ($address in $properties['ProxyAddresses']) { $proxyAddresses += $address.ToString() } } Add-Member -InputObject $r -MemberType NoteProperty -Name ProxyAddresses -Value $proxyAddresses -Force # Add UserPrincipalName If ($SyncObjectType -eq 'User') { $propKey = 'UserPrincipalName' $propValue = $properties[$propKey] if ($propValue -ne $null) { $propValue = $propValue.ToString() } Else { $propValue = "" } Add-Member -InputObject $r -MemberType NoteProperty -Name $propKey -Value $propValue -Force } $results += $r } } } } Until ($batch.AadBatch.MoreToRead -eq $false) #END $enumerator.Dispose() $results } <# .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 [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 ) # BEGIN Import-ADSyncToolsAADConnectorBinaries Write-Verbose "ParameterSetName: $($PSCmdlet.ParameterSetName)" # Check user's UserPrincipalName format Get-ADSyncToolsUPNsuffix -UserPrincipalName $Credential.UserName | Out-Null 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)" } If ($($PSCmdlet.ParameterSetName) -eq 'CsvInput') { 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)" } } Else { $object = "" | Select SyncObjectType,SourceAnchor $object.SyncObjectType = $SyncObjectType $object.SourceAnchor = $SourceAnchor Write-Verbose "ObjectInput: $($object.SyncObjectType) | $($object.SourceAnchor)" $objects = @($object) } Try { $entries = @() ForEach ($obj in $objects) { # Lower 1st char [string] $syncObjectTypeLowerCase = $($obj.SyncObjectType[0].ToString().ToLower()) + $obj.SyncObjectType.Substring(1) Write-Verbose "Processing: DeleteEntry = $syncObjectTypeLowerCase | $($obj.SourceAnchor)" # Add DeleteEntry $entries += [Microsoft.Azure.ActiveDirectory.Connector.Diagnostics.DeletionEntry]::FromSourceAnchor($syncObjectTypeLowerCase, $obj.SourceAnchor) } } Catch { Throw "There was a problem creating DeletionEntry list. 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-ADSyncToolsLdapQuery -RootDN "DC=Contoso,DC=com" -Credential $Credential #> Function Trace-ADSyncToolsLdapQuery { [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 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 Update-ADSyncToolsLogmanLog { [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) { Update-ADSyncToolsLogmanLog -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 = Update-ADSyncToolsLogmanLog '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 ) Update-ADSyncToolsLogmanLog '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( # Wait time to check for the next sync cycle [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=0)] [int] $SleepTimerSecs = 60 ) $script:ADSyncToolsLogmanLogInit = $false 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 "Same 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 "Press CTRL+C any time to stop monitoring Sync Scheduler and type 'Stop-ADSyncToolsLogmanTrace' to stop the current trace." } } While ($true) } #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 $output = "" | select Path,Name,Value $output.Path = $RegPath $output.Name = $RegName If ($regItem -eq $null) { $output.Value = "Not Found" } Else { $output.Value = $regItem.$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 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 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 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 Remove-ADSyncToolsAadObject, ` # Removes orphaned synced object 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-ADSyncToolsLdapQuery, ` # Traces LDAP queries Export-ADSyncToolsHybridAadJoinReport, ` # Generates a report of certificates stored in Active Directory Computer objects Start-ADSyncToolsLogmanTrace, ` # Starts an automated ETW trace on each synchronization cycle Stop-ADSyncToolsLogmanTrace, ` # Stops the automated ETW trace on each synchronization cycle 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 New-ADSyncToolsSqlConnection, ` # SQL Diagnostics Connect-ADSyncToolsSqlDatabase, ` # SQL Diagnostics Invoke-ADSyncToolsSqlQuery,` # SQL Diagnostics Resolve-ADSyncToolsSqlHostAddress, ` # SQL Diagnostics Test-ADSyncToolsSqlNetworkPort, ` # SQL Diagnostics Get-ADSyncToolsSqlBrowserInstances ` # SQL Diagnostics Write-Host "`nADSyncTools for Azure AD 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" |