Using an external program to accomplish a goal is nothing new. In this particular case, I am using repadmin.exe with my current PowerShell session to figure out when a particular user was added into a domain group.
To make all of this happen, I looked at repadmin.exe and use the ShowObjMeta switch which requires a domain controller and the DistinguishedName of the group in question. The output provides an amazing amount of information into a group such as all of the attributes of the group with a last modification date and how many times that attribute has been changed. While this is great, what is more interesting to me is the group membership, which lists not only when a member was added or removed (only members that haven’t tombstoned out) as well as when this change took place and also how many times a modification of adding/removing has taken place for that particular member.
repadmin /showobjmeta dc1 'CN=Domain Admins,CN=Users,DC=rivendell,DC=com'

As you can see, a lot of information is available. One problem though, this is all in text! How can I possibly take this text and make it into something that is actually usable for sorting, filtering, etc…? Simple, do some parsing and make it into an object (something that PowerShell more accustomed to).
The main thing to look at is the Type which has 1 of 3 values:
PRESENT: User currently exists in group and the replicated using Linked Value Replication (LVR).
ABSENT: User has been removed from group and has not been garbage collected based on Tombstone Lifetime (TSL).
LEGACY: User currently exists as a member of the group but has no replication data via LVR.
Another thing is the Version which is how many times a modification of a member has occurred on the group.
With that lets take a look at how we can use PowerShell to make a wrapper around repadmin and return a usable object.
[OutputType('ActiveDirectory.Group.Info')]
[cmdletbinding()]
Param (
[parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,Mandatory=$True)]
[Alias('DistinguishedName')]
[string]$Group,
[parameter()]
[string]$DomainController = ($env:LOGONSERVER -replace "\\\\")
)
This portion sets up the parameters that the function will accept. The Group parameter also has an Alias of DistinguishedName so it can more effectively take pipeline support when using the Get-ADGroup cmdlet from the ActiveDirectory module. The DomainController parameter has a default value of $Env:LOGONSERVER to use whichever domain controller you are currently authenticated against.
Begin {
#RegEx pattern for output
[regex]$pattern = '^(?<State>\w+)` #Gets the State of the member
\s+member(?:\s` #Not worried about the member attribute;we already know its here
(?<DateTime>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})` #Get the modified time
\s+(?:.*\\)?` # Match 1 or none of the backslashes
(?<DC>\w+|(?:(?:\w{8}-(?:\w{4}-){3}\w{12})))` #Match a user readable DC or GUID of DC
\s+(?:\d+)\s+(?:\d+)\s+` #Ignore the USN numbers
(?<Modified>\d+))?' #Number of times modified (added or removed)
}
I intentionally split the string using backticks only for the sake of everything displaying on the webpage (in the function, it is not split). This is the regex pattern that I will use on one of the lines of the repadmin output to get all of the necessary information. Also listed are some comments to define what I am getting out of this pattern. There are no doubt other ways to make this work, but this works just as well for what I need.
Process {
If ($Group -notmatch "^CN=.*") {
Write-Verbose "Attempting to get distinguished name of $Group"
Try {
$distinguishedName = ([adsisearcher]"name=$group").Findone().Properties['distinguishedname'][0]
If (-Not $distinguishedName) {Throw "Fail!"}
} Catch {
Write-Warning "Unable to locate $group"
Break
}
} Else {$distinguishedName = $Group}
Here I check to see if a distinguished name is already being used. If not, then I use adsisearcher to attempt to pull the DN from active directory.
Write-Verbose "Distinguished Name is $distinguishedName"
$data = (repadmin /showobjmeta $DomainController $distinguishedName |
Select-String "^\w+\s+member" -Context 2)
ForEach ($rep in $data) {
If ($rep.line -match $pattern) {
$object = New-Object PSObject -Property @{
Username = [regex]::Matches($rep.context.postcontext,"CN=(?<Username>.*?),.*") |
ForEach {$_.Groups['Username'].Value}
LastModified = If ($matches.DateTime) {[datetime]$matches.DateTime} Else {$Null}
DomainController = $matches.dc
Group = $distinguishedName
State = $matches.state
ModifiedCount = $matches.modified
}
$object.pstypenames.insert(0,'ActiveDirectory.Group.Info')
$object
}
}
}
}
This last part is where I actually call repadmin /showobjmeta with a domain controller and the DN of the group. Using Select-String, I only look for the output that has the member attribute mentioned at the beginning of the line. Because there are 2 lines of output for each member, I use the –Context parameter and specify 2 so it grabs the first and last 2 lines around the main match.

The > shows where the match is at so it is easy to identify. Moving on, I then go through each match and proceed to pull the data using the RegEx pattern defined earlier in the script.
The last piece is that I output an object (finally!) and use .pstypenames.insert() to give this object a new type name. This is so I could apply some sort of formatting if desired for the output. In the end, the output will look like this:
Get-ADGroupMemberDate -Group 'Domain Admins'

Here we can see that bob was removed while Administrator, proxb and Katrina still exist as a member of the Domain Admins group. The domain controller that was used for proxb has since been removed, which is why we see the GUID. Katrina and Administrator were added before there was another Domain Controller in the environment, so it had nothing to replicate to.
Feel free to download this function from the link below and let me know what you think!
Download the function
Script Repository