Freigeben über


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 {            
param($Path)            
    Get-ADOrganizationalUnit -Filter * -SearchBase $Path `
        -SearchScope OneLevel -Server $Server |            
        Sort-Object Name |            
        ForEach-Object {            
            $script:OUHash.Add($_.DistinguishedName,$script:Counter++)            
            Get-ADOrganizationalUnitOneLevel -Path $_.DistinguishedName}            
}            
            
Function Get-ADOrganizationalUnitSorted {            
    $DomainRoot = (Get-ADDomain -Server $Server).DistinguishedName            
    $script:Counter = 1            
    $script:OUHash = @{$DomainRoot=0}            
    Get-ADOrganizationalUnitOneLevel -Path $DomainRoot            
    $OUHash            
}            
            
$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={$_.name.PadLeft($_.name.length + ($_.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       : CohoVineyard.com
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.

Conclusion

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.

Comments

  • Anonymous
    January 27, 2015
    Instead of a search order number you could just store ParentPath calculated from CanonicalName.

    $ParentPath = @{N="ParentPath";E={
    $text = $_.canonicalname
    $split = $text -split "/"
    $split[0..($split.count -2)] -join "/"
    }}
  • Anonymous
    January 28, 2015
    looks like they used a pic of you for another Honorary Scripting Guy lol...
  • Anonymous
    January 28, 2015
    The comment has been removed
  • Anonymous
    February 03, 2015
    Brian,
    You know I didn't even think about the CanonicalName property. That turns out to be the perfect sort field without the need of the recursion routine. (But recursion is still fun.) :-)  You must specify the property name with the -Properties parameter of Get-ADOrganizationalUnit to see it.

Get-ADOrganizationalUnit -Filter * -Properties canonicalname | select canonicalname | sort canonicalname
Thanks!
Ashley
GoateePFE

  • Anonymous
    March 13, 2015
    The comment has been removed
  • Anonymous
    March 13, 2015
    The comment has been removed
  • Anonymous
    April 23, 2015
    Ashley - Thank you. One issue I have - in my environment, running the script (or "Get-GPO -ALL") only returns details for GPOs with User security filtering. If the GPO is strictly tied to Computer objects (e.g. Domain Computers), the script result is blank for DisplayName, GPOStatus,WMIFilter,and GUID data. I am executing as a Domain Admin, using both PSVersion 3.0 w/Win2008 R2 SP1 & PSVersion 4.0 w/loadWin7 Pro. Ever seen this or have any insight on this shortcoming?
  • Anonymous
    May 27, 2015
    Hi Bill,
    This script requires Windows Server 2012 or Windows 8.1 with the RSAT. That might be the hitch.
    Ashley
  • Anonymous
    June 11, 2015
    Do you have Group Policies gone wild? Did you realize too late that it might not be such a good idea to delegate GPO creation to half the IT department? Have you wanted to combine multiple policies into one for simplicity? This blog post is for you.
  • Anonymous
    September 08, 2015
    I know that this is a few months old now but it is a great post (and so is the other post from 2013 which led me to this one). And even though this is a few months old, I still have to ask, is there any way for PS to determine which templates are used by a GPO?

    Thanks again
  • Anonymous
    January 04, 2016
    Hi all, this script is great but due to my orgs structure, it isn't recursing deeply enough, and its returning more than I need. Can anyone point out a way I can start at the root of an OU within the forest and then traverse the entire OU tree beneath it? Thanks!
    • Anonymous
      May 17, 2016
      Looking for Active Directory auditing power shell scripts if you have any Ashley or Jeff. One person left in a client place and need to troubleshoot. 1) Need to check if auditing is turned on 2) If auditing is turned on how can I check a particular user logged on to what devices/ servers? 3) Also if any other details in AD If I can get.Thanks a lot in advance Ashley / Jeff.
  • Anonymous
    January 24, 2017
    The comment has been removed