Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Get Group Policy Links with PowerShell

Posted on January 18, 2021January 18, 2021

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.

Manage and Report Active Directory, Exchange and Microsoft 365 with
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.


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d