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'
    }
}