Forensics: Audit Group Policy Links and Changes with PowerShell

Honorary Scripting Guy Honorary Scripting Guy

I would like to thank Ed and Teresa Wilson, the Microsoft Scripting Guy and the Scripting Wife, for bestowing upon me the title of Honorary Scripting Guy. This was a humbling surprise. It has been a joy to share my scripting passion with the community, and I will continue to do so. Thank you, Ed and Teresa.

In a previous post I created a report of all organizational units (OUs) and sites with their linked group policy objects (GPOs) . This report gives visibility to all of our group policy usage at-a-glance. Since this is one of my most popular downloads I thought it was time to give it a fresh coat of paint. Today I am releasing two significant updates:

  1. After using the script at a customer site recently I noticed that the OU list was in no particular order. Child OUs were listed randomly and not under their parent OUs. Not sure how I missed this the first time around.
  2. In continuing the forensics theme, I thought it would be swell to add some good old fashioned AD Replication Attribute Metadata for tracking the changes to these GPO links.

I don’t know of anywhere else you can find a report like this. Enjoy!

Fixing the Sort Order

It turns out that when you run Get-ADOrganizationalUnit the results are not guaranteed to be in any order. In our mind we’re thinking the list will look like the OU tree from Active Directory Users and Computers (ADUC). But it ain’t so. And there are no cmdlet parameters to create such an ordered output.

This means we’ll have to create our own recursive routine to crawl the OU tree, carefully listing child OUs under the correct parent OUs. This is a classic recursion routine with a function that calls itself. If you’ve not seen one before, then study this one. These functions at the top of the script generate a hash table of each OU and its proper sort order number. I love hash tables for fast look-ups.

Notice how the $Path variable gets recursively populated with the child objects. I used script scope for the counter variable and the OU hash table output. That way nested function calls can update the same values.

 Function Get-ADOrganizationalUnitOneLevel {            
    Get-ADOrganizationalUnit -Filter * -SearchBase $Path `
        -SearchScope OneLevel -Server $Server |            
        Sort-Object Name |            
        ForEach-Object {            
            Get-ADOrganizationalUnitOneLevel -Path $_.DistinguishedName}            
Function Get-ADOrganizationalUnitSorted {            
    $DomainRoot = (Get-ADDomain -Server $Server).DistinguishedName            
    $script:Counter = 1            
    $script:OUHash = @{$DomainRoot=0}            
    Get-ADOrganizationalUnitOneLevel -Path $DomainRoot            
$SortedOUs = Get-ADOrganizationalUnitSorted

In the final Select-Object cmdlet at the end of the script now all we have to do is match the OU distinguished name from Get-ADOrganizationalUnit to the hash table key. This returns the sort order value very quickly.

 $report |            
 Select-Object @{name='OUSort';expression={$SortedOUs[$_.DistinguishedName]}}, `
  @{name='SOM';expression={$$ + ($_.depth * 5),'_')}}, `
  DistinguishedName, BlockInheritance, LinkEnabled, Enforced,  . . |            
 Sort-Object OUSort, Precedence, SOM |            
 Export-CSV .\gPLink_Report_Sorted_Metadata.csv -NoTypeInformation

We’ll pipe the columns out to a sort now, and it’s good to go.

Adding Replication Metadata (a.k.a. the juicy forensics)

Ever since the Get-ADReplicationAttributeMetadata cmdlet was released in Windows Server 2012 I have used it frequently for forensic reports. (You can see how it works over at the MVA AD PowerShell videos here; look at module four on forensics.) This cmdlet returns several properties, but here are the ones I am including in the report for gPLink auditing:

LastOriginatingChange DirectoryServerIdentity Human-readable DC name CN=NTDS Settings, CN=CVDCR2, CN=Servers, CN=Ohio, CN=Sites, CN=Configuration, DC=CohoVineyard, DC=com
LastOriginatingChange DirectoryServerInvocationId DC database ID 4eab0674-680c-4036-851a-1ba76275ca01
LastOriginatingChange Time Last change date and time 11/20/2014 12:39:58 PM
Version How many times has this gPLink been updated? (Example: 1 for creation, plus 22 updates.) 23

gPLink is the AD attribute on a domain, OU, or site that contains a list of all the GPOs linked. I explained more about this attribute in the previous post.

Note that in cases where multiple GPOs are linked to one location these gPLink report details are duplicated for each GPO. This is because a single gPLink attribute lists all linked policies. The implementation is less-than-ideal, but that is the design we have to work with. The weakness here is that we cannot see exactly which policy was linked or unlinked at that time. We just know it was one of those in the list. Then you can use the other GPO dates as clues.

In addition to these details, I am going back to the Get-GPO output to pull in the date and version information for each linked policy.

 PS C:\> Get-GPO "Default Domain Policy"

DisplayName      : Default Domain Policy
DomainName       :
Owner            : COHOVINEYARD\Domain Admins
Id               : 31b2f340-016d-11d2-945f-00c04fb984f9
GpoStatus        : AllSettingsEnabled
Description      : 
CreationTime     : 4/12/2011 1:37:16 PM
ModificationTime : 10/3/2014 12:14:30 PM
UserVersion      : AD Version: 0, SysVol Version: 0
ComputerVersion  : AD Version: 38, SysVol Version: 38
WmiFilter        : 

All of these new columns follow the other helpful columns from the original report: block inheritance, link enabled, enforced, precedence, WMI filter, etc.

Forensic Clues

Given this intersection of GPO-specific data with gPLink data we can now observe some interesting findings:

  • gPLinks with versions and dates but no GPO listed indicate that all GPOs were unlinked from that location at the reported date and time.
  • High version numbers on the gPLink indicate frequent churn on the policies linked to the OU.
  • High version numbers on the DS/SYSVOL columns show you the most frequently updated policies.

When you carefully study this data, a story emerges. For example, you can see that on the last change control date the new policy was unlinked from the test OU and then linked to the production OU. Cool!

Remember that this report only reflects linked policies. You likely have other test policies that are not linked anywhere and therefore not shown in this report.

Upgrade Time

It is important to note that the previous version of this script ran on Windows Server 2008 R2 and above, or Windows 7, with the RSAT for Active Directory PowerShell installed. Since I added the Get-ADReplicationAttributeMetadata cmdlet, you must now use Windows Server 2012 and above, or Windows 8.1, with RSAT for Active Directory PowerShell installed. This can be an admin workstation or a tools server. As a matter of best practice you should not log on directly to domain controllers to run scripts like this.


This scripting solution involves a number of fun elements:

  • Get-GPO
  • Get-ADReplicationAttributeMetadata
  • Computer Science Programming 101 recursion
  • Variable scoping

I hope that you not only learned about GPO forensics, but you also have some new techniques for your next challenge.  Happy scripting!

You can download the full script at the TechNet Script Center here.  The download includes sample CSV output to view the finished product.