Identify ADMX/ADML Files used by Group Policies

The Problem

Group Policy ADMX versioning has caused a few concerns for Microsoft customers in the past one to two years. A great description of the issue and how to address it is found here.

Recently, one of my customers wanted to identify the ADMX files referenced by Group Policies deployed in their domain so that they could carefully update the ADMX/ADML central store in SYSVOL. This isn't entirely easy because the ADMX file is not used when the GPO is applied to a computer or user.

Further Explanation

The ADMX/ADML file combination may be thought of as the description language that instructs the Group Policy editor. The Group Policy editor shows the policy settings, policy descriptions, drop down boxes and radio buttons that the ADMX/ADML files tell it to. Configuring a policy setting results in the Group Policy editor making changes to a .pol (or other) file in SYSVOL that is ultimately applied to the computer or user.

When you generate a Group Policy report, all of the ADMX and ADML files are read and compared with the policy settings stored in SYSVOL. This information is then glued together by the report generator and output as either HTML or XML. No record of the ADML/ADML files required for the report is kept.

The Solution

I managed to construct a PowerShell script that consumes an XML-based GPO Report, parses out all of the settings, reads the ADML store specified in the script and matches settings to ADMLs. ADML file names correspond to ADMX file names used by the Group Policy editor. The output is a CSV (also specified in the script) that looks similar to -

gpoName settingScope settingName admlFile
PolicyA Computer Site to Zone Assignment List InetRes.adml
PolicyB User Allow DFS roots to be published SharedFolders.adml
PolicyA User Add Logoff to the Start Menu StartMenu.adml
PolicyA Computer Show lock in the user tile menu WindowsExplorer.adml
PolicyC Computer Allow remote server management through WinRM WindowsRemoteManagement.adml
PolicyD Computer Register domain joined computers as devices WorkplaceJoin.adml

 

The script assumes a language match between the GPO Report and the ADML file path provided in the script and has a dependency on the Group Policy PowerShell module.

If you wish to use the following code, redefine <OUTPUT_PATH> and <FULLY_QUALIFIED_DOMAIN_NAME> according to your own environment (I know I could have parameterised this but I'm lazy and this is just a sample).

Also note that something whacky is going on with the source code plugin colour formatting below. I can't work it out but the script still works …

The Script

 # Define results file
$results = "<OUTPUT PATH>\Results.csv"

# Define PolicyDefinition ADML Folder
$policyDefs = "\\<FULLY_QUALIFIED_DOMAIN_NAME>\SYSVOL\<FULLY_QUALIFIED_DOMAIN_NAME>\Policies\PolicyDefinitions\en-US"

# Generate a GPO report and capture it as XML
[xml]$GPOs = Get-GPOReport -All -ReportType Xml

# Parse captured XML
$policyInfo = @()

for ($i = 0; $i -lt ($GPOs.DocumentElement.GPO.Count); $i++)
{ 
    #Process Computer Policy
    for ($j = 0; $j -lt $GPOs.DocumentElement.GPO[$i].Computer.ExtensionData.ChildNodes.Count; $j++)
    { 
        if (($GPOs.DocumentElement.GPO[$i].Computer.ExtensionData.ChildNodes[$j].type) -like "*:RegistrySettings")
        {
            if (!($GPOs.DocumentElement.GPO[$i].Computer.ExtensionData.ChildNodes[$j].Policy.Count -eq $null))
            {
                for ($k = 0; $k -lt $GPOs.DocumentElement.GPO[$i].Computer.ExtensionData.ChildNodes[$j].Policy.Count; $k++)
                { 
                    $polInfo = "" | Select-Object gpoName, settingScope, settingName
                    $polInfo.gpoName = $GPOs.DocumentElement.GPO[$i].Name
                    $polInfo.settingScope = "Computer"
                    $polInfo.settingName = $GPOs.DocumentElement.GPO[$i].Computer.ExtensionData.ChildNodes[$j].Policy[$k].Name
                    $policyInfo += $polInfo
                }
            }
            else
            {
                $polInfo = "" | Select-Object gpoName, settingScope, settingName
                $polInfo.gpoName = $GPOs.DocumentElement.GPO[$i].Name
                $polInfo.settingScope = "Computer"
                $polInfo.settingName = $GPOs.DocumentElement.GPO[$i].Computer.ExtensionData.ChildNodes[$j].Policy.Name
                $policyInfo += $polInfo
            }
        }
    }
    #Process User Policy
    for ($j = 0; $j -lt $GPOs.DocumentElement.GPO[$i].User.ExtensionData.ChildNodes.Count; $j++)
    { 
        if (($GPOs.DocumentElement.GPO[$i].User.ExtensionData.ChildNodes[$j].type) -like "*:RegistrySettings")
        {
            if (!($GPOs.DocumentElement.GPO[$i].User.ExtensionData.ChildNodes[$j].Policy.Count -eq $null))
            {
                for ($k = 0; $k -lt $GPOs.DocumentElement.GPO[$i].User.ExtensionData.ChildNodes[$j].Policy.Count; $k++)
                { 
                    $polInfo = "" | Select-Object gpoName, settingScope, settingName
                    $polInfo.gpoName = $GPOs.DocumentElement.GPO[$i].Name
                    $polInfo.settingScope = "User"
                    $polInfo.settingName = $GPOs.DocumentElement.GPO[$i].User.ExtensionData.ChildNodes[$j].Policy[$k].Name
                    $policyInfo += $polInfo
                }
            }
            else
            {
                $polInfo = "" | Select-Object gpoName, settingScope, settingName
                $polInfo.gpoName = $GPOs.DocumentElement.GPO[$i].Name
                $polInfo.settingScope = "User"
                $polInfo.settingName = $GPOs.DocumentElement.GPO[$i].User.ExtensionData.ChildNodes[$j].Policy.Name
                $policyInfo += $polInfo
            }
        }
    }
}

# Define output array
$admlFileUsage = @()

# Search ADML files for policy settings
$admlFiles = Get-ChildItem -Path $policyDefs -Filter *.adml

foreach ($admlFile in $admlFiles)
{
    $admlContent = (Get-Content -Path ($admlFile.FullName))
    $out = "" | Select-Object gpoName, settingScope, settingName, admlFile
    foreach ($polInfo in $policyInfo)
    {
        $settingName = $polInfo.settingName
        if ($admlContent -like "*$settingName*")
        {
            $out.gpoName = $polInfo.gpoName
            $out.settingScope = $polInfo.settingScope
            $out.settingName = $polInfo.settingName
            $out.admlFile = $admlFile.Name
            $admlFileUsage += $out
        }
    }
}

$admlFileUsage | Export-Csv -Path $results -NoTypeInformation -Force

Conclusion

I hope that this script serves as a useful example for others to build upon. As I've only used this script in a test lab with a small number of Group Policy objects, it's entirely possible I've overlooked something. If you identify a problem or see room for improvement, I'd be happy to take the feedback.