RepAdmin.psm1
|
function Get-TimeDelta { <# .SYNOPSIS Detects the time drift between the local and the specified computer. .DESCRIPTION Detects the time drift between the local and the specified computer. .PARAMETER ComputerName The computers to test. .PARAMETER Credential The credentials to use. Will only be used for the configuration fallback detection method. .PARAMETER Samples How many time samples should be taken, in order to calculate the time drift. Defaults to: 25 .PARAMETER ThrottleLimit Up to how many computers should be scanned in parallel. Defaults to 25 .EXAMPLE PS C:\> Get-TimeDelta -ComputerName dc1.contoso.com Checks the time drift of dc1.contoso.com #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [string[]] $ComputerName, [PSCredential] $Credential, [int] $Samples = 9, [int] $ThrottleLimit = 25 ) begin { #region Code $code = { param ($ComputerName) $credParam = @{ } if ($credential) { $credParam.Credential = $credential } $offsetData = w32tm /stripchart /computer:$ComputerName /samples:$samples /dataonly /rdtsc 2>$null | Where-Object { $_ -match '^\S+,' } | ConvertFrom-Csv $success = $LASTEXITCODE -eq 0 $offset = ($offsetData.NtpOffset | ForEach-Object { $_ -as [double] } | Measure-Object -Average).Average $roundtrip = ($offsetData.RoundtripDelay | ForEach-Object { $_ -as [double] } | Measure-Object -Average).Average $source = "<Unknown>" $configData = w32tm /query /status /computer:$ComputerName 2>$null if ($LASTEXITCODE -ne 0) { $configData = Invoke-Command -ComputerName $ComputerName @credParam -ScriptBlock { w32tm /query /status } -ErrorAction SilentlyContinue } # Only works on en-US machines, or those otherwise not localized, sorry if ($configData -match 'Source: ') { $line = $configData -match 'Source: ' $source = ($line -split '\s', 2)[-1] } [PSCustomObject]@{ PSTypeName = 'RepAdmin.TimeDelta' ComputerName = $ComputerName Success = $success Offset = $offset Roundtrip = $roundtrip Source = $source } } #endregion Code $variables = @{ samples = $Samples credential = $Credential } $command = { Invoke-PSFRunspace -Scriptblock $code -Variables $variables -ThrottleLimit $ThrottleLimit }.GetSteppablePipeline() $command.Begin($true) } process { foreach ($name in $ComputerName) { $command.Process($name) } } end { $command.End() } } function Resolve-ForestDomainController { <# .SYNOPSIS Retrieve all DCs of a forest. .DESCRIPTION Retrieve all DCs of a forest. .PARAMETER Server The domain / server to work against. .PARAMETER Credential The credentials to use for the request. .EXAMPLE PS C:\> Resolve-ForestDomainController Retrieve all DCs of the current user's forest. #> [CmdletBinding()] param ( [string] $Server, [AllowNull()] [PSCredential] $Credential ) $param = @{} if ($Server) { $param.Server = $Server } if ($Credential) { $param.Credential = $Credential } $forest = Get-ADForest @param $domains = foreach ($domain in $forest.Domains) { Get-ADDomain -Identity $domain @param } $dcs = Get-ADDomainController @param -Filter * [PSCustomObject]@{ Name = $forest.Name Forest = $forest Domains = $domains RootPDC = @($domains).Where{ $_.DnsRoot -eq $forest.Name }.PDCEmulator PDCEmulator = $domains.PDCEmulator RIDMaster = $domains.RIDMaster InfrastructureMaster = $domains.InfrastructureMaster SchemaMaster = $forest.SchemaMaster DomainNamingMaster = $forest.DomainNamingMaster All = $dcs.HostName } } function Resolve-GpoServer { <# .SYNOPSIS Resolve which server to use for Group Policy operations. .DESCRIPTION Resolve which server to use for Group Policy operations. Result Paths: - With "AllDCs" set: All DCs in the domain targeted through "Server", otherwise the current user's domain. - With "Server" targeting a specific DC: That DC - With "Server" targeting a domain: That DCs PDC Emulator - Otherwise: The Current User's Domain's PDC Emulator Will return a splatting hashtable for PS remoting purposes. .PARAMETER Server The server to target. May be a domain or specific server. .PARAMETER Credential The credentials to use for all requests. .PARAMETER AllDCs Retrieve all DCs for the domain. .EXAMPLE PS C:\> $param = Resolve-GpoServer -Server $Server -Credential $Credential -AllDCs:$AllDCs Resolve which server to use for Group Policy operations. #> [OutputType([hashtable])] [CmdletBinding()] param ( [AllowEmptyString()] [string] $Server, [AllowNull()] [PSCredential] $Credential, [switch] $AllDCs ) $param = @{} $target = @{} if ($Server) { $param.Server = $Server } if ($Credential) { $param.Credential = $Credential $target.Credential = $Credential } if ($AllDCs) { $dcs = Get-ADComputer -LDAPFilter '(primaryGroupID=516)' @param $target.ComputerName = $dcs.DNSHostName return $target } $adDomain = Get-ADDomain @param if (-not $Server) { $target.ComputerName = $adDomain.PDCEmulator return $target } if ($Server -ne $adDomain.DNSRoot) { $target.ComputerName = $Server return $target } $target.ComputerName = $adDomain.PDCEmulator $target } function Get-RaADObjectAttributeReplication { <# .SYNOPSIS Tracks the replication progress of a single attribute change, as it propagates across Active Directory domain controllers. .DESCRIPTION Tracks the replication progress of a single attribute change, as it propagates across Active Directory domain controllers. This is attempted by polling all domain controllers for for the replication metadata for the specified attribute. Unfortunately, Active Directory domain controllers do not actually maintain a USN/Timestamp match, hence this toolkit will ... - use the metadata to find whether the replication was received - use the "WhenChanged" property on the object _after_ that replication was received, to determine the timestamp. This means, that if some time has passed and the object has been modified again in the meantime (maybe in another attribute), this new timestamp will be reported, leading to later timestamps than expected. In short: This tool is for live troubleshooting. It is NOT suitable for forensics trying to look further into the past! .PARAMETER Identity The identity of the object to troubleshoot. Expects a unique ID "Get-ADObject" accepts, so DN or ObjectGUID should work, SamAccountName should not. .PARAMETER Attribute The attribute to track. Note: This parameter is CASE SENSITIVE and expects the LDAP name, not the DisplayName in PowerShell. E.g.: "Description" will fail, "description" works. .PARAMETER Server The domain to work against. Even when specifying a single server, all servers in that server's domain will be polled. .PARAMETER Credential The credentials to use for the request. .PARAMETER Timeout How long should the command wait before giving up in case of replication errors. Defaults to 30 minutes. Domain Controllers that did not receive the replicated object will be reported with an empty timestamp. .EXAMPLE PS C:\> Get-RaADObjectAttributeReplication -Identity $user -Attribute description Tracks the replication of the "description" attribute change across the domain. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Identity, [Parameter(Mandatory = $true)] [string] $Attribute, [string] $Server, [pscredential] $Credential, [timespan] $Timeout = '00:30:00' ) begin { $adParam = @{} if ($Server) { $adParam.Server = $Server } if ($Credential) { $adParam.Credential = $Credential } $domainControllers = Get-ADComputer @adParam -LDAPFilter '(primaryGroupID=516)' $adParam.Remove('Server') } process { $pending = @{} # Determine Minimum Version $attributeStats = foreach ($domainController in $domainControllers) { $pending[$domainController.DNSHostName] = $domainController try { Get-ADReplicationAttributeMetadata @adParam -Server $domainController.DNSHostName -Object $Identity -Properties $Attribute } catch { } } if (-not $attributeStats) { throw "AD Object '$Identity' (or attribute '$Attribute') not found!" } $minVersion = ($attributeStats | Measure-Object Version -Maximum).Maximum $limit = (Get-Date).Add($Timeout) do { foreach ($domainController in $($pending.Keys)) { try { $attributeItem = Get-ADReplicationAttributeMetadata @adParam -Server $domainController -Object $Identity -Properties $Attribute } catch { $attributeItem = $null } if (-not $attributeItem -or $attributeItem.Version -lt $minVersion) { continue } $adObject = Get-ADObject @adParam -Server $domainController -Identity $Identity -Properties uSNChanged, WhenChanged [PSCustomObject]@{ Server = $domainController Identity = $Identity Timestamp = $adObject.WhenChanged Attribute = $Attribute Version = $attributeItem.Version LocalUSN = $adObject.uSNChanged OriginUSN = $attributeItem.LastOriginatingChangeUsn OriginTimestamp = $attributeItem.LastOriginatingChangeTime OriginServer = $attributeItem.LastOriginatingChangeDirectoryServerIdentity -replace '^CN=NTDS Settings,CN=(.+?),.+', '$1' } $pending.Remove($domainController) } if ((Get-Date) -lt $limit) { Start-Sleep -Seconds 1 continue } foreach ($domainController in $pending.Keys) { Write-Warning "Timeout on DC $DomainController, replication still not completed!" try { $attributeItem = Get-ADReplicationAttributeMetadata @adParam -Server $domainController -Object $Identity -Properties $Attribute } catch { $attributeItem = $null } [PSCustomObject]@{ Server = $domainController Identity = $Identity Timestamp = $null Attribute = $Attribute Version = $attributeItem.Version LocalUSN = $null OriginUSN = $attributeItem.LastOriginatingChangeUsn OriginTimestamp = $attributeItem.LastOriginatingChangeTime OriginServer = $attributeItem.LastOriginatingChangeDirectoryServerIdentity -replace '^CN=NTDS Settings,CN=(.+?),.+', '$1' } } break } until ($pending.Count -lt 1) } } function Get-RaADServerReplication { <# .SYNOPSIS List replication status between domain controllers in an Active Directory domain. .DESCRIPTION List replication status between domain controllers in an Active Directory domain. .PARAMETER Server The domain to scan. Even when specifying an explicit domain controller, it will enumerate all replication links of all DCs in the domain it is part of. .PARAMETER Credential The credentials to use on AD requests. .EXAMPLE PS C:\> Get-RaADServerReplication List replication status between domain controllers in the current Active Directory domain. #> [CmdletBinding()] param ( [string] $Server, [pscredential] $Credential ) begin { $adParam = @{} if ($Server) { $adParam.Server = $Server } if ($Credential) { $adParam.Credential = $Credential } $domainControllers = Get-ADComputer @adParam -LDAPFilter '(primaryGroupID=516)' $adParam.Remove("Server") } process { foreach ($domainController in $domainControllers) { try { $metadata = Get-ADReplicationPartnerMetadata @adParam -Scope Server -Target $domainController.DNSHostName } catch { [PSCustomObject]@{ Source = $domainController.DNSHostName Partition = $null From = $null To = $null USN = $null LastAttempt = $null LastSuccess = $null LastCode = $null ErrorCount = $null Object = $_ } continue } foreach ($datum in $metadata) { if ($datum.PartnerType -eq 'Inbound') { $from = $datum.Partner -replace 'CN=NTDS Settings,CN=(.+?),.+', '$1' $to = $datum.Server } else { $from = $datum.Server $to = $datum.Partner -replace 'CN=NTDS Settings,CN=(.+?),.+', '$1' } [PSCustomObject]@{ Source = $domainController.DNSHostName Partition = $datum.Partition From = $from To = $to USN = $datum.LastChangeUsn LastAttempt = $datum.LastReplicationAttempt LastSuccess = $datum.LastReplicationSuccess LastCode = $datum.LastReplicationResult ErrorCount = $datum.ConsecutiveReplicationFailures Object = $datum } } } } } function Get-RaDCTimeDelta { <# .SYNOPSIS Scans all DCs in an Active Directory forest and returns their time delta compared to the Root Domain PDC Emulator. .DESCRIPTION Scans all DCs in an Active Directory forest and returns their time delta compared to the Root Domain PDC Emulator. Use this to analyze time sync issues in an Active Directory forest. All DCs in a forest should orient their time against the PDC Emulator in the root domain. Too large a drift can lead to service disruption and invalidity of tickets. + The Delta is the time difference between the individual DC and its Root PDC + The Offset is the time difference between the individual DC and the local computer + The Roundtrip is the time it takes from the local computer to the individual dc and back + The Source is where the DC looks for its current time. This allows detecting misconfigurations, if it looks elsewhere but the root PDC. .PARAMETER Server The domain / server to work against. All DCs in the forest(s) this target is part of will be scanned. .PARAMETER AllForests Scan all forests. This enumerates all AD trusts to figure out all known forests, then perfors the scan against all of them. .PARAMETER Exclude List of forests to NOT scan. Use together with -AllForests to exclude specific reachable forests. .PARAMETER Credential The credentials to use for the request. .PARAMETER ThrottleLimit Hopw many scans to do in parallel. Defaults to: 25 .EXAMPLE PS C:\> Get-RaDCTimeDelta Get the time-drift of all DCs in the current user's forest. #> [CmdletBinding(DefaultParameterSetName = 'Target')] param ( [Parameter(ParameterSetName = 'Target')] [PSFComputer[]] $Server = $env:USERDNSDOMAIN, [Parameter(ParameterSetName = 'Scan')] [switch] $AllForests, [string[]] $Exclude, [PSCredential] $Credential, [int] $ThrottleLimit = 25 ) begin { function ConvertTo-RelativeTimeDelta { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $InputObject, [Parameter(Mandatory = $true)] $Reference ) process { foreach ($item in $InputObject) { [PSCustomObject]@{ PSTypeName = 'RepAdmin.RelTimeDelta' ComputerName = $item.ComputerName Success = $item.Success Offset = $item.Offset Delta = $Reference.Offset - $item.Offset Roundtrip = $item.Roundtrip Source = $item.Source } } } } } process { $domains = $Server if ($AllForests) { $domains = Get-RaForest -Credential $Credential } foreach ($domain in $domains) { if ($domain -in $Exclude) { continue } $domainControllers = Resolve-ForestDomainController -Server $domain -Credential $Credential $dcOffsets = $domainControllers.All | Get-TimeDelta -ThrottleLimit $ThrottleLimit $dcOffsets | ConvertTo-RelativeTimeDelta -Reference ($dcOffsets | Where-Object ComputerName -EQ $domainControllers.RootPDC) } } } function Get-RaForest { <# .SYNOPSIS Search for forests, following up on all trust links. .DESCRIPTION Search for forests, following up on all trust links. .PARAMETER ExcludeOwn Exclude the root forest from the results returned. By default, both the root forest, as well as all forests linked to it via trusts are returned. .PARAMETER AsObject Return the list of forest as forest objects, rather than just the names. .PARAMETER Server The domain / server to work against. This target is used as the root forest, from which trusts are enumerated to find linked forests. .PARAMETER Credential The credentials to use for the request. .EXAMPLE PS C:\> Get-RaForest returns all forest names from the current and all linked forests. #> [OutputType([string])] [CmdletBinding(DefaultParameterSetName = 'Default')] param( [switch] $ExcludeOwn, [switch] $AsObject, [string] $Server, [AllowNull()] [PSCredential] $Credential ) $param = @{} $credParam = @{} if ($Server) { $param.Server = $Server } if ($Credential) { $param.Credential = $Credential $credParam.Credential = $Credential } $trusts = (Get-ADTrust @param -Filter *).Name $ownForest = Get-ADForest @param $forests = foreach ($trust in $trusts) { try { Get-ADForest @credParam -Server $trust -ErrorAction Stop } catch { Write-Error "Failed to resolve forest for trust $($trust): $_" } } $allForests = @($forests) + @($ownForest) | Sort-Object Name -Unique $allForests | Where-Object { -not $ExcludeOwn -or $_.Name -ne $ownForest.Name } | ForEach-Object { if ($AsObject) { $_ } else { $_.Name } } } function Get-RaGpoMetadata { <# .SYNOPSIS Gathers version information about GPOs, either for all GPOs on one server or one GPO acros all servers. .DESCRIPTION Gathers version and access rule consistency information about GPOs. This scan allows scanning ... - One GPO or multiples or all - On one DC or all of them In conclusion, this enables you to scan for issues such as the existence of replication problems. Both AD and SYSVOL side. Note: Rapid changes to a GPO inbetween replication cycles can lead to a divergence in SYSVOL version numbers. .PARAMETER Server The domain / server to work against. .PARAMETER Credential The credentials to use for the request. .PARAMETER AllDCs Select all DCs for the specified domain. .PARAMETER Name Name of the GPO to filter for. Defaults to: * .EXAMPLE PS C:\> Get-RaGpoMetadata Retrieves the integrity data for all GPOs from the current domain's PDCEmulator .EXAMPLE PS C:\> Get-RaGpoMetadata -AllDCs Retrieves the integrity data for all GPOs from all DCs in the current domain .EXAMPLE PS C:\> Get-RaGpoMetadata -AllDCs -Server contoso.com -Name 'Default Domain Policy' Retrieves the integrity data for the "Default Domain Policy" from all DCs in the "contoso.com" domain #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [CmdletBinding()] param ( [string] $Server, [PSCredential] $Credential, [switch] $AllDCs, [string] $Name = '*' ) $param = Resolve-GpoServer -Server $Server -Credential $Credential -AllDCs:$AllDCs Invoke-Command @param -ScriptBlock { param ($Name) $system = Get-CimInstance -ClassName win32_computersystem $fqdn = '{0}.{1}' -f $system.Name, $system.Domain Get-GPO -All -Server localhost -Domain $system.Domain | Where-Object DisplayName -Like $Name | ForEach-Object { [PSCustomObject]@{ ComputerName = $fqdn PolicyName = $_.DisplayName PolicyId = $_.Id Domain = $_.DomainName Owner = $_.Owner AclConsistent = $(try { $_.IsAclConsistent() } catch {}) ConfigConsistent = ($_.User.DSVersion -eq $_.User.SysvolVersion) -and ($_.Computer.DSVersion -eq $_.Computer.SysvolVersion) UserADVersion = $_.User.DSVersion UserSysvolVersion = $_.User.SysvolVersion ComputerADVersion = $_.Computer.DSVersion ComputerSysvolVersion = $_.Computer.SysvolVersion Created = $_.CreationTime Modified = $_.ModificationTime } } } -ArgumentList $Name } function Invoke-RaGpoAclConsistency { <# .SYNOPSIS Fix inconsistent access rules on GPOs. .DESCRIPTION Fix inconsistent access rules on GPOs. Detects cases, where the permissions for GPOs in Active Directory do not match the ones in SYSVOL. Fixes all issues found, but does not modify anything not affected. Use Get-RaGpoMetadata to find all affected GPOs without applying remediation. .PARAMETER Server The domain / server to work against. .PARAMETER Credential The credentials to use for the request. .PARAMETER Name Name of the GPO to check and remediate if needed. Defaults to: * .EXAMPLE PS C:\> Invoke-RaGpoAclConsistency Fix inconsistent access rules on all GPOs in the current domain. .EXAMPLE PS C:\> Invoke-RaGpoAclConsistency -Server contoso.com Fix inconsistent access rules on all GPOs in the contoso.com domain. #> [CmdletBinding()] param ( [string] $Server, [PSCredential] $Credential, [string] $Name = '*' ) $param = Resolve-GpoServer -Server $Server -Credential $Credential Invoke-Command @param -ScriptBlock { param ($Name) $system = Get-CimInstance -ClassName win32_computersystem $fqdn = '{0}.{1}' -f $system.Name, $system.Domain Get-GPO -All -Server localhost -Domain $system.Domain | Where-Object DisplayName -Like $Name | ForEach-Object { $consistent = $_.IsAclConsistent() $result = [PSCustomObject]@{ ComputerName = $fqdn PolicyName = $_.DisplayName PolicyId = $_.Id WasConsistent = $consistent IsConsistent = $consistent Success = $true Error = $null } if ($consistent) { return $result } try { $_.MakeAclConsistent() $result.IsConsistent = $_.IsAclConsistent() $result.Success = $result.IsConsistent } catch { $result.Success = $false $result.IsConsistent = $null $result.Error = $_ } $result } } -ArgumentList $Name } function Invoke-RaRepAdmin { <# .SYNOPSIS Executes the repadmin commandline tool againast remote computers. .DESCRIPTION Executes the repadmin commandline tool againast remote computers. Remote execution done via PowerShell remoting. .PARAMETER ComputerName The computers to execute repadmin on. Defaults to: localhost .PARAMETER Credential Credentials to use for the remoting connection. .PARAMETER ArgumentList The arguments to pass to repadmin.exe .EXAMPLE PS C:\> repadmin /showrepl Shows the replication status of the domain of the current computer, ad requests executing from the local computer. .EXAMPLE PS C:\> repadmin -ComputerName dc1.contoso.com, dc1.fabrikam.org /showrepl Shows the replication status of the contoso.com and fabrikam.org domains. #> [Alias('repadmin')] [CmdletBinding(PositionalBinding = $false)] param ( [PSFComputer[]] $ComputerName = $env:COMPUTERNAME, [pscredential] $Credential, [Parameter(Position = 0, ValueFromRemainingArguments = $true)] [string[]] $ArgumentList ) process { Invoke-PSFCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock { param ($Data) $argumentList = $Data.Args $result = RepAdmin.exe @argumentList *>&1 [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME Success = $LASTEXITCODE -eq 0 Result = ($result -join "`n").Trim() } } -ArgumentList @{ Args = $ArgumentList } | Select-PSFObject -KeepInputObject -TypeName 'RepAdmin.Result' } } |