I was chatting with my friend Gladys Kravitz about Group Policy reporting stuff recently,. and the discussion led me to dust off some old code I had for getting Group Policy links using PowerShell. The GroupPolicy module has a Set-GPLink command, but nothing that easily shows you what GPOs are linked to your site, domain and OUs. Even though you may not need such a report, I'm hoping there is something in my scripting techniques that you will find helpful.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Start with a Core Command
As with any PowerShell project, you want to start from the console. Can you run a command interactively that give you the essence of your goal? In my case, this is the Get-GPInheritance cmdlet from the GroupPolicy module. I'll count on your to read help and examples. Here's how it shows me what is linked at the domain level.
In looking at the output, I see a property that probably has the information I'm after.
This looks promising. I can verify it works with organizational units as well.
(Get-ADOrganizationalUnit -filter * | Get-GPInheritance).GpoLinks |
Select-Object -Property Target,DisplayName,Enabled,Enforced,Order |
Format-Table
This looks pretty good and is close to what I was hoping to accomplish.
Querying Site Links
What about sites? Assuming I don't know the distinguished names of all my sites, I want a programmatic solution. I can use the ActiveDirectory module to discover sites.
$getADO = @{
LDAPFilter = "(Objectclass=site)"
properties = "Name"
SearchBase = (Get-ADRootDSE).ConfigurationNamingContext
}
$sites = Get-ADObject @getADO
I only have a single site but unfortunately Get-GPInheritance can't use sites. There are ways to dig through Active Directory and parse out site links, but I decided to take an easier way and fall back on the legacy COM object, GPMGMT.GPM. Here's how.
First, I need an object reference. I'm also going to get all of the constants the object requires.
$gpm = New-Object -ComObject "GPMGMT.GPM"
$gpmConstants = $gpm.GetConstants()
I'm going to need references to the domain and forest.
$gpmdomain = $gpm.GetDomain("company.pri", "", $gpmConstants.UseAnyDC)
$SiteContainer = $gpm.GetSitesContainer("company.pri", "company.pri", $null, $gpmConstants.UseAnyDC)
My domain and forest are called Company.pri.
The $SiteContainer object has a GetSite() method, but it needs the name of a site. But I got that earlier.
This new object has a method called GetGPOLinks().
That's pretty good. All I'm missing is the GPO name.
$site.GetGPOLinks() | Select GPOId,@{Name="DisplayName";Expression = {$gpmdomain.GetGPO($_.gpoid).Displayname}},Enabled,Enforced,SOMLinkOrder
Since I have the GPOId, I can get the GPO. This code is using the COM object. I could have used the cmdlet, but I would need to parse out the {} characters.
$site.GetGPOLinks() | Select GPOId,@{Name="DisplayName";Expression = {(Get-GPO -id ($_.gpoid.replace("{|",""))).Displayname}},Enabled,Enforced,SOMLinkOrder
With these pieces in place I can build a PowerShell tool around these code snippets.
Get-GPLink
I wrote a function called Get-GPLink. Since I am using the GroupPolicy and ActiveDirectory cmdlets, and they support specifying a server and/or domain, I wanted to support that as well. Initially, I tried to use splatting and dynamically assign parameter values. But this quickly became unwieldy. Instead, I define a script-scope version of $PSDefaultParameter values.
if ($Server) {
$script:PSDefaultParameterValues["Get-AD*:Server"] = $server
$script:PSDefaultParameterValues["Get-GP*:Server"] = $Server
}
if ($domain) {
$script:PSDefaultParameterValues["Get-AD*:Domain"] = $domain
$script:PSDefaultParameterValues["Get-ADDomain:Identity"] = $domain
$script:PSDefaultParameterValues["Get-GP*:Domain"] = $domain
}
Now I don't have to worry about it If I run Get-ADDomain and the Server parameter is used from my function, the Get-ADDomain cmdlet will automatically use it.
The function goes through and queries the domain and organizational units using the GroupPolicy module. Site level links are retrieved using the COM object. Technically, I could have used the COM object for everything.
The function collects all the links which allows me to offer filtering such by GPO name or to show only enabled or disabled links.
That's not too bad. But I always want more. For example, I can't pipe the output of my function to Get-GPO. Although I can, if I make a minor adjustment.
But I don't want to do that all the time. I'd also like a default output of a table. I know I can do that with a custom format.ps1xml file and my trusty New-PSFormatXML command. But to do that I need a unique and custom type name.
In my code, I can insert a new type name since I'm not creating a custom object from scratch as I usually do.
$results.GetEnumerator().ForEach( { $_.psobject.TypeNames.insert(0, "myGPOLink") })
Once I have a new type name, I can define type extensions.
Update-TypeData -MemberType AliasProperty -MemberName GUID -Value GPOId -TypeName myGPOLink -Force
Update-TypeData -MemberType AliasProperty -MemberName Name -Value DisplayName -TypeName myGPOLink -Force
Update-TypeData -MemberType AliasProperty -MemberName GPO -Value DisplayName -TypeName myGPOLink -Force
Update-TypeData -MemberType AliasProperty -MemberName Link -Value Target -TypeName myGPOLink -Force
Update-TypeData -MemberType AliasProperty -MemberName Domain -Value GpoDomainName -TypeName myGPOLink -Force
Update-TypeData -MemberType ScriptProperty -MemberName TargetType -Value {
switch -regex ($this.target) {
"^((ou)|(OU)=)" { "OU" }
"^((dc)|(DC)=)" { "Domain" }
"^((cn)|(CN)=)" { "Site" }
Default { "Unknown"}
}
} -TypeName myGPOLink -Force
This makes it much easier now to work with my Group Policy links.
Custom Format
Of course, since I have a custom type I can create a format.ps1xml file and get really fancy. Now I can create a default table view of the results. And since I'm always looking for ways to add color to PowerShell I decided to highlight disabled links and those that are enforced using ANSI escape sequences.
<TableColumnItem>
<ScriptBlock>
<!-- use ANSI formatting if using the console host-->
if ($host.name -eq 'ConsoleHost') {
if ($_.Enabled) {
$_.Enabled
}
else {
"$([char]0x1b)[1;91m$($_.enabled)$([char]0x1b)[0m"
}
}
else {
$_.Enabled
}
</ScriptBlock>
</TableColumnItem>
The scriptblock only uses ANSI if PowerShell detects a console host. I'm using an escape sequence that will work in both Windows PowerShell and PowerShell 7.x
The format file includes other view as well.
Summary
I use the workflow I've just described in much of my work. I start with core commands, build a robust wrapper around the commands in the form of a function, write an object to the pipeline, and if necessary customize with type and format extensions. The end result should be an easy to use and helpful PowerShell tool that runs the in the pipeline like any other PowerShell command.
If you want to see how this all works, I've posted the code as a GitHub gist. If you want to try it out, you'll need to save both files to the same directory. Pay close attention to the name of the format file since that is loaded when you dot source the function file.
If you have any questions about what I did or why, please feel free to leave a comment.