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!
i have repadmin. i use a domain admin account. i ran the script on my workstation where i usually ran PS script. i didn’t get any output at all.
Pingback: AD Group Auditing with Powershell :: Craig Porteous
i would like to keep the entire CN of the object, how can i keep that?
I noticed when using this it and grabs the data:
ModifiedCount : 3
DomainController : SIDC
LastModified : 11/9/2015 1:21:31 PM
Username : test user
State : PRESENT
Group : CN=testGroup1,OU=Groups,DC=SID,DC=com
The username is actually the Displayname that it grabs, any way to change it so that it grabs the SamAccountName?
You would have to add some queries to the existing code so it can pull the account information from AD. This is currently only what is available from the metadata that is replicated.
Hi Boe, excellent script! Thanks so much. I don’t get to use PowerShell nearly as much as I’d prefer, but I kludged the following in to return SamAccountName instead of Username, which for us was returning “Lastname\, ” when querying.
Function Get-ADGroupMemberDate {
<#
.SYNOPSIS
Provides the date that a member was added to a specified Active Directory group.
}
Hi Boe, excellent script! I don’t get to use PowerShell nearly as much as I’d like, but I kludged the following which seems to return the same data, just with SamAccountName instead of the “Lastname\, ” my folks were getting before. The only problem is that nested subgroups are not properly traversed or handled, just skipped.
Function Get-ADGroupMemberDate {
<#
.SYNOPSIS
Provides the date that a member was added to a specified Active Directory group.
}
Whoops – and forgot to say thanks for the script!
That is awesome! Thanks for sharing that excellent update. I’ll try to get that updating on technet one of these days. Thanks!
does this script work? when i run “Get-ADGroupMemberDate” its not a cmdlet that is recognized? what am i missing? If anyone has made this script work, please advise steps…
thanks JohnM
How are you running the script? Can you provide the exact syntax? Did you make sure to dot source the file first to load the function?
Is there any way to find who added that account into a security group through PowerShell?
when using this function the Modifiedcount,domaincontoller & lastmodified objects are empty, running repadmin from the command prompt/ps prompt returns the required values but the function doesn’t, running on Windows 2008 R2
I’ll test this and see what the issue is. Thanks for the heads up!
I’m not able to get this to return any information. The repadmin works fine but using the Get-ADGroupMemberDate doesn’t return anything for me. What am I missing here?
I’ve tried running this as well with the same results. Doesn’t matter which group I run it on.
Repadmin only returns 20 members of a group when I run it, running it on “Domain Users” in a real world domain, not a test domain.
How do I get the rest of the 3600+ members?
Hmm, must be something special about this group, other large groups return all their members with Repadmin.
Amazing function btw! Thanks for sharing… Any way to get the UserName to be the Sam Account Name attribute from AD?
Instead of repadmin how about using Get-ADReplicationAttributeMetadata?
Its funny you mention that. Someone was showing that at the Hey Scripting Guy booth yesterday at Tech Ed. From what I have read, it is only available on Server 2012 though. It would definitely be the way to go on those systems.
ah didn’t know that you could possibly just switch to get-adobject -filter msDS-ReplAttributeMetaData then? and then parse that
Perfect! That is something that i will try once I get back home. Thanks!
actually might need msDS-ReplValueMetaData instead/aswell
Cool script Boe, nice parsing on the Repadmin data. Just downloaded the script and the output is quite nice.