I've been working on cleaning up Group Policies for a couple of months. While it may seem trivial, things get complicated when you're tasked with managing 5000 GPOs created over 15 years by multiple teams without any best practices in mind. While working on GPOZaurr (my new PowerShell module), I've noticed that the more code I wrote to manage those GPOs, the more I knew passing this knowledge to admins who will be executing this on a weekly/monthly basis is going to be a challenge. That's why I've decided to follow a similar approach as my other Active Directory testing module called Testimo. I've created a single command that analyses Group Policies using different methods and shows views from different angles to deliver the full picture. On top of that, it provides a solution (or it tries to) so that it's fairly easy to fix – as long as you agree with what it proposes.
Please keep in mind I've tested GPOZaurr only on English based Active Directory. I have no clue how it will behave on non-English systems. As I've not worked with other languages for a while, I don't remember if object types are still reported in English by PowerShell or reported in language equivalent. Be careful.
Please make sure to visit GitHub to review sources or report issues. If you're going to use it, I recommend doing it via PowerShellGallery as that version is minimized and optimized. Reviewing sources is easier on the GitHub version as it has more comments and is divided into sections.
The module is signed with a certificate, like any new modules that I create or update.
Install-Module GPOZaurr -Force
As mentioned before, Invoke-GPOZaurr follows a similar pattern to what Invoke-Testimo does. When run without any parameters, it will go thru all available reports one by one to deliver a full-scope scan. Keep in mind that running this cmdlet without any parameters is fine for small domains, but it will take hours to complete for larger domains. For the domain of 5000 GPOs, some reports can take even 2 hours to complete.
When run, it will display a short information about what it is currently doing and which report is being generated. If you have a large domain and things take time, you may want to use Invoke-GPOZaurr with Verbose parameter to get additional information.
Once the cmdlet is complete HTML report will open up automatically.
As you can see on the screenshot above, multiple reports were created, each on a different tab. The design of the report is mostly the same. There is information about what the report detected and why it did so on the report's top left. It also gives you a summary of your whole forest and where the issues are found. In the top right corner, I've added a small chart that visualizes the current status. Some charts will show only problems. Some will show multiple statues – all depending on the type of report getting generated. There is usually one, but sometimes more tables with displayed information depending on the problem in the second section. Tables are color-coded to visualize better what is bad or to distinguish multiple problems within the same report. Tables also allow you to export data to Excel, CSV or PDF.
Finally, the last section contains the solution to the problem described. It usually provides step by step instructions on fixing the problem if you choose to fix it. Most of the time, solutions are automated to the point where a single line of code can fix an issue. For example, delete all empty GPOs, delete all unlinked GPOs, and so on. One command, zero effort.
Currently, Invoke-GPOZaurr has few built-in reports. Some of them are more advanced, some of them are for review only. Here's the full list for today. Not everything is 100% finished. Some will require some updates soon as I get more time and feedback. Feel free to report issues/improve those reports with more information.
Group Policies are stored in two places – Active Directory (metadata) and SYSVOL (content). Since those are managed in different ways, replicated in different ways, it's possible because of different issues, and they get out of sync.
Invoke-GPOZaurr -Type GPOBroken
With just a few simple steps, you can have that fixed in a couple of minutes. Keep in mind that you need to have healthy replication of group policies for this to work and not report false positives. If you have unhealthy replication and wrong, DC will get asked about those issues you could potentially remove legitimate content.
When GPO is deleted correctly, it usually is removed from AD, SYSVOL, and any link to it is also discarded. Unfortunately, this is true only if the GPO is created and linked within the same domain. If GPO is linked in another domain, this leaves a broken link hanging on before it was linked. Additionally, the Remove-GPO cmdlet doesn't handle site link deletions, which causes dead links to be stuck on sites until those are manually deleted. This means that any GPOs deleted using PowerShell may leave a trail.
Invoke-GPOZaurr -Type GPOBrokenLink
Invoke-GPOZaurr -Type GPOOwners
This report is fairly complete with detection and automated fix.
When GPO is created, it creates an entry in Active Directory (metadata) and SYSVOL (content). Two different places mean two different sets of permissions. The group Policy module is making sure the data in both places is correct. However, it's not necessarily the case for different reasons, and often permissions go out of sync between AD and SYSVOL. This test verifies the consistency of policies between AD and SYSVOL in two ways. It checks top-level permissions for a GPO and then checks if all files within said GPO is inheriting permissions or have different permissions in place.
Invoke-GPOZaurr -Type GPOConsistency
This report is fairly complete and with an automated fix.
CNF objects, Conflict objects, or Duplicate Objects are created in Active Directory when there is simultaneous creation of an AD object under the same container on two separate Domain Controllers near about the same time or before the replication occurs. This results in a conflict and a CNF (Duplicate) object exhibits the same. While it doesn't necessarily have a huge impact on Active Directory, it's important to keep Active Directory in a proper, healthy state.
Invoke-GPOZaurr -Type GPODuplicates
This report is fairly complete and with an automated fix. Be advised above screenshot doesn't show any detected problems because it's pretty hard to generate duplicated objects on-demand, so my test environment doesn't have any. But it does detect those.
Over time Administrators add more and more group policies as business requirements change. Due to neglect or thinking it may serve its purpose, later on, many Group Policies often have no value at all. Either the Group Policy is not linked to anything and stays unlinked forever, or GPO is linked, but the link (links) are disabled, or GPO is totally disabled. Then there are Group Policies that are targetting certain groups or persons, and that group is removed, leaving Group Policy doing nothing. Additionally, sometimes new GPO is created without any settings, or the settings are removed over time, but GPO stays in place.
Invoke-GPOZaurr -Type GPOList
This report is fairly complete and provides automated fixes for most issues detected.
The following report contains a full overview of all permissions around Group Policies. It detects 4 different problems (lack of authenticated users, wrong permissions for Domain Admins and Enterprise Admins, Unknown permissions, and lack of proper permission for SYSTEM account). It also contains all permissions, so it's easy to review all permissions from a single place. For each problem, automation is developed, so it's fairly easy to fix any issues as long as you agree with what's proposed.
Invoke-GPOZaurr -Type GPOPermissions
This report is interactive, meaning clicking on a GPO in one table limits permissions shown in another table. GPOPermissions type is kind of ultimate way for you to deal with permissions. I've made one report that covers what 3 different reports were covering before.
Invoke-GPOZaurr -Type GPOPermissionsRead,GPOPermissionsAdministrative,GPOPermissionsUnknown
So while you can use the cmdlet above with each type separately – it's easier to use one.
Invoke-GPOZaurr is basically a wrapper of around 20 or so different GPO cmdlets that I have developed over a period of six months. I was worried that with so many cmdlets being available in my module and my laziness in the documentation, I thought Invoke-GPOZaurr's three-step approach (Describe Problem, Provide Data, Offer Solution) was an experiment that I believe will help me manage my GPOs efficiently for years to come. Not everything is completed, but at the current state, it's good enough for release. It allows you to understand where you stand without spending days, weeks, or months of analysis depending on how big your Active Directory is. Of course, this one little command has few more options that allow for different customization options.
Invoke-GPOZaurr [[-Type] <string[]>] [[-ExcludeGroupPolicies] <scriptblock>] [-FilePath <string>] [-PassThru] [-HideHTML] [-HideSteps] [-ShowError] [-ShowWarning] [-Forest <string>] [-ExcludeDomains <string[]>] [-IncludeDomains <string[]>] [<CommonParameters>]
Using a Type parameter, you can ask for one or multiple types. Providing FilePath parameter, you can tell GPOZaurr where to save created HTML file. PassThru, on the other hand, is useful to have HTML generated and get the output of the reports back to you for future analysis.
It's also possible to hide steps to fix a given problem. This can be useful if you're doing an overview for your Client/Management and don't want to show how to fix it.
Invoke-GPOZaurr -FilePath $Env:UserProfile\Desktop\Test.html -Type GPOBroken -HideSteps
Using HideHTML parameter prevents auto-opening of HTML. It's useful for automation purposes.
Invoke-GPOZaurr -FilePath $Env:UserProfile\Desktop\Test.html -Type GPOBroken -HideSteps -HideHTML
GPO Analysis report is one of the coolest ones I've made. It's able to provide a lot of smaller reports that show the content of group policies. Each report is a separate tab. Using GPO GUI, you would normally show you similar output, but this one does it globally. If you've ever tried to find all GPOs that map drives, find ones that have script execution – it's the way to go.
Invoke-GPOZaurr -Type GPOAnalysis
The idea for every report is that each setting is stored per each line. This sometimes means that if the setting has a potential of 50 options, the report will generate 50 columns. I've not found an easy way to make it readable without custom creating and every report. While I do that for some of the reports, some are totally autogenerated. If you feel something is not covered or require a better report, open up an issue, and we can see what can be done.
Since I want to keep my group policies healthy at all times, I've developed small automation. This automation deals with one report and sends an email to a ticketing system if there is a problem or sends an update to the AD team that everything is great. This automation uses PSWriteHTML (which is also used to generate HTML anyway). I've developed the module where the description on each report is available to use outside of GPOZaurr (that's where the PassThru parameter is useful).
Import-Module GPOZaurr -Force $PasswordSecureString = 'passwordSecureString' $Types = @( @{ Name = 'GPOOwners' Path = "$PSScriptRoot\Reports\GPOOwners_$(Get-Date -f yyyy-MM-dd_HHmmss).html" Subject = '[AD Compliance] Group Policy Owners Issue' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $true } @{ Name = 'GPODuplicates' Path = "$PSScriptRoot\Reports\GPODuplicates_$(Get-Date -f yyyy-MM-dd_HHmmss).html" Subject = '[AD Compliance] Group Policy Duplicate (Conflicting) Objects Detected' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $true } @{ Name = 'NetLogonOwners' Path = "$PSScriptRoot\Reports\NetLogonPermissions_$(Get-Date -f yyyy-MM-dd_HHmmss).html" Subject = '[AD Compliance] NetLogon Owners Issue' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $true } @{ Name = 'GPOConsistency' Path = "$PSScriptRoot\Reports\GPOConsistency_$(Get-Date -f yyyy-MM-dd_HHmmss).html" Subject = '[AD Compliance] Group Policy Consistency' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $true } # Too big @{ Name = 'GPOPermissions' Path = "$PSScriptRoot\Reports\GPOPermissions_$(Get-Date -f yyyy-MM-dd_HHmmss).html" Subject = '[AD Compliance] Group Policy Permissions Analysis' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $false } @{ Name = 'GPOList' Path = "$PSScriptRoot\Reports\GPOList_$(Get-Date -f yyyy-MM-dd_HHmmss).html" Subject = '[AD Compliance] Group Policy Empty & Unlinked & Disabled Cleanup' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $false } @{ Name = 'GPOBroken'; Path = "$PSScriptRoot\Reports\GPOOrphans_$(Get-Date -f yyyy-MM-dd_HHmmss).html"; Subject = '[AD Compliance] Group Policy Orphaned/Broken Cleanup' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $true } @{ Name = 'GPOBrokenLink' Path = "$PSScriptRoot\Reports\GPOBrokenLink_$(Get-Date -f yyyy-MM-dd_HHmmss).html" Subject = '[AD Compliance] Group Policy Broken Links' Ticket = '[Ticket#2001000](https://linkToChangeRequest)' Attach = $true } ) foreach ($Type in $Types) { $EmailHeaderBadReport = EmailHeader { EmailFrom -Address '[email protected]' EmailTo -Addresses "[email protected]", '[email protected]' EmailServer -Server 'smtpServer' -SSL -Port 25 -UserName 'login' -Password $PasswordSecureString -PasswordAsSecure EmailOptions -Priority High -DeliveryNotifications Never EmailSubject -Subject $Type.Subject if ($Type.Attach -eq $true) { EmailAttachment -FilePath $Type.Path } } $EmailHeaderGoodReport = EmailHeader { EmailFrom -Address '[email protected]' EmailTo -Addresses "[email protected]", '[email protected]' EmailServer -Server 'smtpServer' -SSL -Port 25 -UserName 'login' -Password $PasswordSecureString -PasswordAsSecure EmailOptions -Priority Low -DeliveryNotifications Never EmailSubject -Subject $Type.Subject if ($Type.Attach -eq $true) { EmailAttachment -FilePath $Type.Path } } $ReportOutput = Invoke-GPOZaurr -FilePath $Type.Path -Type $Type.Name -PassThru -HideHTML -Verbose foreach ($Report in $ReportOutput.Keys | Where-Object { $_ -notin 'Version', 'Settings' }) { if ($ReportOutput[$Report]['ActionRequired'] -eq $true) { Email { $EmailHeaderBadReport EmailBody { EmailText -Text 'Hello Team,' -LineBreak EmailText -Text "I've found disprepency in our domain that needs to be fixed and I need your help. " -LineBreak $ReportOutput[$Report]['Summary'] EmailText -LineBreak EmailText -TextBlock { "This automation was approved by CAB in $($Type.Ticket). " "The goal is to keep Group Policies Healthy at all times! " "In case of issues please contact Przemyslaw Klys " } -LineBreak EmailText -Text 'With regards,' EmailText -Text 'Automated Monitoring' } -FontSize 10pt } } else { Email { $EmailHeaderGoodReport EmailBody { EmailText -Text 'Hello Team,' -LineBreak EmailText -Text "I've run the report and everything is looking great. Nothing to do here, but just wanted to say - great job! " -LineBreak $ReportOutput[$Report]['Summary'] EmailText -LineBreak EmailText -TextBlock { "This automation was approved by CAB in $($Type.Ticket). " "The goal is to keep Group Policies Healthy at all times! " "In case of issues please contact Przemyslaw Klys " } -LineBreak EmailText -Text 'With regards,' EmailText -Text 'Automated Monitoring' } -FontSize 10pt } } } }
Keep in mind that some of those reports can get really large. For example, the permissions report for 4000 GPOs is about 30MB in size. On the other hand, some other reports are much smaller. This is why there's an option to choose whether to attach a report or not.
GPOZaurr is a huge module. It contains a lot of reports, and just a handful of those are shown here. It's almost 20000 lines of code. It can deal with all sorts of GPO/SYSVOL/NETLOGON problems you may have. Feel free to explore. On GitHub, the full source code is available (and somewhat readable – one function per file) and about 40 different examples. Not everything may be easy to understand, but I plan to release more blog posts on different ways to deal with issues. What's important to know is that this module will work just fine with just user credentials. Of course, if you've removed authenticated users from a GPO, some reports will skip it, others will mark it as unavailable, but it does work. Of course, fixing issues will require Domain Admin, but that you can do manually – not even running GPOZaurr as Domain Admin.
The module is signed with a certificate, like any new modules that I create or update.
Install-Module GPOZaurr -Force
GO Ahead! Have fun! Make sure to report any issues, or if you feel like something would require covering more ground, let me know. My goal is to have GPOZaurr as the only way to deal with Group Policies.