Public/Group/Get-ADGroupMembershipMetadata.ps1

<#
    .SYNOPSIS
    Get the replication metadata of the membership of an Active Directory group: when each member was added,
    when it was removed, and on which domain controller.
 
    .DESCRIPTION
    Group membership is a linked value, so its history is not carried by msDS-ReplAttributeMetaData but by
    msDS-ReplValueMetaData, which holds one entry per value of the link, including the values that were
    removed. That makes it the only way to answer "who was added to this group last week, and who was removed
    from it" without a security event log.
 
    Removed members remain visible until the tombstone lifetime of the forest has elapsed, 180 days by default
    on a forest created in Windows Server 2003 SP1 or later.
 
    The metadata is local to the domain controller answering the query, hence -Server.
 
    Requires the ActiveDirectory module (RSAT).
 
    .PARAMETER Identity
    The distinguished name of the group. Accepts pipeline input by property name, so Get-ADGroup can be piped
    into this function.
 
    .PARAMETER Server
    Domain controller to query. Defaults to a discovered DC of the current domain.
 
    .PARAMETER DeletedOnly
    Keeps only the members that were removed from the group.
 
    .PARAMETER CurrentOnly
    Keeps only the members currently in the group.
 
    .EXAMPLE
    Get-ADGroupMembershipMetadata 'CN=Domain Admins,CN=Users,DC=example,DC=com'
 
    The whole membership history of the group, current and removed members alike.
 
    .EXAMPLE
    Get-ADGroupMembershipMetadata 'CN=Domain Admins,CN=Users,DC=example,DC=com' -DeletedOnly
 
    Who was removed from Domain Admins, and when.
 
    .EXAMPLE
    Get-ADGroup -Filter 'adminCount -eq 1' | Get-ADGroupMembershipMetadata | Where-Object { $_.TimeCreated -gt (Get-Date).AddDays(-30) }
 
    Every addition to a privileged group over the last thirty days.
 
    .LINK
    https://github.com/bastienperez/PSADDS
#>

function Get-ADGroupMembershipMetadata {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias('GroupDN', 'DistinguishedName')]
        [string]$Identity,

        [Parameter(Mandatory = $false)]
        [Alias('DomainController')]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [switch]$DeletedOnly,

        [Parameter(Mandatory = $false)]
        [switch]$CurrentOnly
    )

    begin {
        if ($DeletedOnly.IsPresent -and $CurrentOnly.IsPresent) {
            throw 'The -DeletedOnly and -CurrentOnly parameters are mutually exclusive.'
        }

        if (-not $Server) {
            $Server = (Get-ADDomainController -Discover -Service ADWS).HostName
            Write-Verbose "No domain controller specified, using the discovered one: $Server"
        }
    }

    process {
        try {
            $group = Get-ADObject -Identity $Identity -Properties 'msDS-ReplValueMetaData' -Server $Server -ErrorAction Stop
        }
        catch {
            Write-Warning "Unable to read the group '$Identity': $($_.Exception.Message)"
            return
        }

        $replValueMetaData = ConvertFrom-ADReplMetadata -RawMetadata $group.'msDS-ReplValueMetaData'

        if (-not $replValueMetaData) {
            Write-Warning "No linked value metadata returned by $Server for $($group.DistinguishedName): the group never had a member, or its links predate the metadata"
            return
        }

        foreach ($metadata in $replValueMetaData) {
            # msDS-ReplValueMetaData covers every linked attribute of the object, keep the membership only
            if ($metadata.pszAttributeName -and $metadata.pszAttributeName -ne 'member') {
                continue
            }

            $timeDeleted = ConvertTo-ADReplMetadataDate -Timestamp $metadata.ftimeDeleted

            if ($DeletedOnly.IsPresent -and $null -eq $timeDeleted) {
                continue
            }

            if ($CurrentOnly.IsPresent -and $null -ne $timeDeleted) {
                continue
            }

            [PSCustomObject][ordered]@{
                GroupDN                   = $group.DistinguishedName
                MemberDN                  = $metadata.pszObjectDn
                IsDeleted                 = ($null -ne $timeDeleted)
                TimeCreated               = ConvertTo-ADReplMetadataDate -Timestamp $metadata.ftimeCreated
                TimeDeleted               = $timeDeleted
                Version                   = [int]$metadata.dwVersion
                TimeLastOriginatingChange = ConvertTo-ADReplMetadataDate -Timestamp $metadata.ftimeLastOriginatingChange
                LastOriginatingDsaDN      = $metadata.pszLastOriginatingDsaDN
                FromDomainController      = $Server
            }
        }
    }
}