Public/Get-GPOInventoryWithSettings.ps1

function Get-GPOInventoryWithSettings {
<#
.SYNOPSIS
    Generates a full GPO inventory for an Active Directory domain
    and exports it as a styled HTML report, including configured GPO settings.

.DESCRIPTION
    Collects the following for every GPO in the domain:
    - Creation and modification timestamps
    - OU links, including domain root links
    - Computer and user settings status
    - Applied permissions and trustees
    - WMI filter name and query
    - Configured settings inside the GPO from Get-GPOReport XML

    The ConfiguredSettings column shows settings configured in the GPO.
    It does not calculate Resultant Set of Policy (RSOP), link precedence,
    inheritance, security filtering outcome, or WMI filter pass/fail outcome
    for a specific computer or user.

.PARAMETER DomainName
    The fully qualified domain name to inventory.
    Example: "corp.contoso.com"

.PARAMETER DomainController
    Optional domain controller to query for AD and GPO data.
    If omitted, the function uses the domain PDC emulator.
    Example: "DC01.corp.contoso.com"

.PARAMETER OutputPath
    Path to write the HTML report.
    Defaults to $env:TEMP\GPOInventoryWithSettings.html

.PARAMETER SaveGpoReportXmlFolder
    Optional folder where the raw Get-GPOReport XML for each GPO is saved.
    Use this when a GPO has settings but the ConfiguredSettings column still
    does not show the expected details.

.EXAMPLE
    Get-GPOInventoryWithSettings -DomainName "corp.contoso.com"

.EXAMPLE
    Get-GPOInventoryWithSettings -DomainName "corp.contoso.com" -OutputPath "D:\Reports\GPOInventoryWithSettings.html"

.EXAMPLE
    Get-GPOInventoryWithSettings -DomainName "corp.contoso.com" -DomainController "DC01.corp.contoso.com"

.EXAMPLE
    Get-GPOInventoryWithSettings -DomainName "corp.contoso.com" -SaveGpoReportXmlFolder "C:\temp\GPOXml"

.NOTES
    Author: K Shankar R Karanth
    Website: https://karanth.ovh
    Version: 2.1
    Requires: ActiveDirectory module, GroupPolicy module,
              read access to AD and GPO objects,
              run as Domain Admin or equivalent
#>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$DomainName,

        [string]$DomainController,

        [string]$OutputPath = "C:\ADOpsKit\Reports\Get-GPOInventoryWithSettings\$(Get-Date -Format 'yyyy-MM-dd')_GPOInventoryWithSettings.html",

        [string]$SaveGpoReportXmlFolder
    )

    function Get-GuidFromGpLinkLocal {
        param(
            [string]$GpLink
        )

        if ([string]::IsNullOrWhiteSpace($GpLink)) {
            return @()
        }

        [regex]::Matches($GpLink, '\{[0-9A-Fa-f-]{36}\}') |
            ForEach-Object { $_.Value.Trim('{}').ToLowerInvariant() }
    }

    function Convert-GpoIdToKey {
        param(
            [Parameter(Mandatory = $true)]
            [object]$GpoId
        )

        try {
            return ([guid]$GpoId).ToString().ToLowerInvariant()
        }
        catch {
            return $GpoId.ToString().Trim('{}').ToLowerInvariant()
        }
    }

    function ConvertTo-CompactText {
        param(
            [AllowNull()]
            [string]$Text,

            [int]$MaxLength = 240
        )

        if ([string]::IsNullOrWhiteSpace($Text)) {
            return $null
        }

        $value = ($Text -replace '\s+', ' ').Trim()

        if ($value.Length -gt $MaxLength) {
            return $value.Substring(0, $MaxLength) + "..."
        }

        return $value
    }

    function ConvertTo-SafeFileName {
        param(
            [Parameter(Mandatory = $true)]
            [string]$Name
        )

        $invalidChars = [System.IO.Path]::GetInvalidFileNameChars()
        $safeName = $Name

        foreach ($char in $invalidChars) {
            $safeName = $safeName.Replace($char, '_')
        }

        return $safeName
    }

    function Get-XmlElementChildren {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node
        )

        return @(
            $Node.ChildNodes |
                Where-Object { $_.NodeType -eq [System.Xml.XmlNodeType]::Element }
        )
    }

    function Get-DirectChildText {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node,

            [Parameter(Mandatory = $true)]
            [string[]]$Names
        )

        foreach ($name in $Names) {
            $child = Get-XmlElementChildren -Node $Node |
                Where-Object { $_.LocalName -ieq $name } |
                Select-Object -First 1

            if ($child) {
                $value = ConvertTo-CompactText -Text $child.InnerText

                if ($value) {
                    return $value
                }
            }
        }

        return $null
    }

    function Get-FirstAttributeText {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node,

            [Parameter(Mandatory = $true)]
            [string[]]$Names
        )

        if (-not $Node.Attributes) {
            return $null
        }

        foreach ($name in $Names) {
            foreach ($attribute in $Node.Attributes) {
                if ($attribute.LocalName -ieq $name) {
                    $value = ConvertTo-CompactText -Text $attribute.Value

                    if ($value) {
                        return $value
                    }
                }
            }
        }

        return $null
    }

    function Get-XmlAttributePairs {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node,

            [string[]]$SkipNames = @()
        )

        $pairs = [System.Collections.Generic.List[string]]::new()

        if (-not $Node.Attributes) {
            return @()
        }

        foreach ($attribute in $Node.Attributes) {
            if ($attribute.Name -like 'xmlns*') {
                continue
            }

            if ($attribute.Prefix -in @('xmlns', 'xsi', 'xsd')) {
                continue
            }

            if ($SkipNames -icontains $attribute.LocalName) {
                continue
            }

            $value = ConvertTo-CompactText -Text $attribute.Value

            if ($value) {
                $pairs.Add(("{0}={1}" -f $attribute.LocalName, $value))
            }
        }

        return @($pairs)
    }

    function Get-XmlLeafPairs {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node,

            [string[]]$SkipNames = @()
        )

        $pairs = [System.Collections.Generic.List[string]]::new()

        foreach ($child in Get-XmlElementChildren -Node $Node) {
            if ($SkipNames -icontains $child.LocalName) {
                continue
            }

            $grandChildren = Get-XmlElementChildren -Node $child

            if ($grandChildren.Count -gt 0) {
                continue
            }

            $value = ConvertTo-CompactText -Text $child.InnerText

            if ($value) {
                $pairs.Add(("{0}={1}" -f $child.LocalName, $value))
            }
        }

        return @($pairs)
    }

    function Get-XmlNestedSummaryPairs {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node,

            [int]$MaxItems = 12
        )

        $pairs = [System.Collections.Generic.List[string]]::new()
        $skipContainers = @(
            'Category',
            'Explain',
            'Properties',
            'Supported',
            'SupportedOn'
        )
        $skipLeafNames = @(
            'Category',
            'Explain',
            'Supported',
            'SupportedOn'
        )

        foreach ($child in Get-XmlElementChildren -Node $Node) {
            if ($skipContainers -icontains $child.LocalName) {
                continue
            }

            $grandChildren = Get-XmlElementChildren -Node $child

            if ($grandChildren.Count -eq 0) {
                continue
            }

            $childPairs = @(
                (Get-XmlAttributePairs -Node $child) +
                (Get-XmlLeafPairs -Node $child -SkipNames $skipLeafNames)
            )

            if ($childPairs.Count -gt 0) {
                $pairs.Add(("{0}({1})" -f $child.LocalName, ($childPairs -join ', ')))
            }

            if ($pairs.Count -ge $MaxItems) {
                break
            }
        }

        return @($pairs)
    }

    function Test-IsPolicySettingNode {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node
        )

        if ($Node.NodeType -ne [System.Xml.XmlNodeType]::Element) {
            return $false
        }

        $localName = $Node.LocalName
        $excludedNames = @(
            'Category',
            'DisplayName',
            'Enabled',
            'Explain',
            'Extension',
            'ExtensionData',
            'Filter',
            'FilterDataAvailable',
            'Filters',
            'Name',
            'Properties',
            'State',
            'Supported',
            'SupportedOn',
            'VersionDirectory',
            'VersionSysvol'
        )

        if ($excludedNames -icontains $localName) {
            return $false
        }

        $knownSettingContainers = @(
            'Account',
            'AccountLockout',
            'AdministrativeTemplate',
            'Application',
            'Audit',
            'AuditPolicy',
            'AuditSetting',
            'BitLocker',
            'CertificateSettings',
            'DataSource',
            'DeployedPrinterConnection',
            'Drive',
            'Efs',
            'EnvironmentVariable',
            'EventLog',
            'File',
            'FileSystem',
            'FirewallRule',
            'FirewallSettings',
            'Folder',
            'FolderRedirection',
            'Group',
            'ImmediateTask',
            'Ini',
            'Ipsec',
            'Kerberos',
            'KerberosPolicy',
            'LocalGroup',
            'LocalUser',
            'NetworkOptions',
            'Package',
            'Password',
            'PasswordPolicy',
            'Policy',
            'Printer',
            'PublicKey',
            'QoSPolicy',
            'Registry',
            'RegistryKey',
            'RegistrySettings',
            'RestrictedGroups',
            'ScheduledTask',
            'Script',
            'SecuritySetting',
            'SecurityOption',
            'SecurityOptions',
            'Service',
            'SharedPrinter',
            'Shortcut',
            'SoftwareInstallation',
            'SystemServices',
            'TcpIpPrinter',
            'UserRightsAssignment',
            'WiredNetworkPolicy',
            'WirelessNetworkPolicy'
        )

        if ($knownSettingContainers -icontains $localName) {
            return $true
        }

        $children = Get-XmlElementChildren -Node $Node
        $attributePairs = @(Get-XmlAttributePairs -Node $Node -SkipNames @('clsid', 'uid', 'image', 'changed'))

        if ($Node.Attributes) {
            foreach ($attribute in $Node.Attributes) {
                if (@('action', 'displayName', 'hive', 'key', 'name', 'path', 'policyState', 'sourcePath', 'status', 'targetPath', 'type', 'value', 'valueName') -icontains $attribute.LocalName) {
                    return $true
                }
            }
        }

        if ($children.Count -eq 0) {
            return ($attributePairs.Count -gt 0)
        }

        if ($Node.SelectSingleNode("./*[local-name()='Properties']")) {
            return $true
        }

        $settingName = Get-DirectChildText -Node $Node -Names @('DisplayName', 'Name', 'PolicyName', 'Title')

        if ($settingName) {
            $settingState = Get-DirectChildText -Node $Node -Names @('Action', 'Enabled', 'PolicyState', 'State', 'Type')

            if ($settingState) {
                return $true
            }

            foreach ($child in $children) {
                if ($child.LocalName -like 'Setting*') {
                    return $true
                }
            }

            if ($children.Count -le 10) {
                return $true
            }
        }

        return $false
    }

    function Get-PolicySettingCandidateNodes {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Root
        )

        $results = [System.Collections.Generic.List[System.Xml.XmlNode]]::new()

        function Add-PolicySettingNode {
            param(
                [Parameter(Mandatory = $true)]
                [System.Xml.XmlNode]$Node,

                [Parameter(Mandatory = $true)]
                [AllowEmptyCollection()]
                [System.Collections.Generic.List[System.Xml.XmlNode]]$ResultList
            )

            foreach ($child in Get-XmlElementChildren -Node $Node) {
                if (Test-IsPolicySettingNode -Node $child) {
                    $ResultList.Add($child)
                    continue
                }

                Add-PolicySettingNode -Node $child -ResultList $ResultList
            }
        }

        Add-PolicySettingNode -Node $Root -ResultList $results

        return @($results)
    }

    function Format-PolicySetting {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$Node,

            [Parameter(Mandatory = $true)]
            [string]$ScopeName,

            [Parameter(Mandatory = $true)]
            [string]$ExtensionName
        )

        $settingType = $Node.LocalName
        $propertiesNode = $Node.SelectSingleNode("./*[local-name()='Properties']")
        $settingName = Get-DirectChildText -Node $Node -Names @('DisplayName', 'Name', 'PolicyName', 'Title')

        if (-not $settingName -and $propertiesNode) {
            $settingName = Get-FirstAttributeText -Node $propertiesNode -Names @(
                'displayName',
                'name',
                'targetPath',
                'path',
                'key',
                'valueName',
                'sourcePath',
                'location'
            )
        }

        if (-not $settingName) {
            $settingName = Get-FirstAttributeText -Node $Node -Names @(
                'displayName',
                'name',
                'targetPath',
                'path',
                'key',
                'valueName',
                'sourcePath',
                'location'
            )
        }

        if (-not $settingName) {
            $settingName = $settingType
        }

        $detailList = [System.Collections.Generic.List[string]]::new()
        $state = Get-DirectChildText -Node $Node -Names @('Action', 'Enabled', 'State')

        if ($state) {
            $detailList.Add(("State={0}" -f $state))
        }

        foreach ($pair in Get-XmlAttributePairs -Node $Node -SkipNames @('clsid', 'uid')) {
            $detailList.Add($pair)
        }

        if ($propertiesNode) {
            foreach ($pair in Get-XmlAttributePairs -Node $propertiesNode -SkipNames @('clsid', 'uid')) {
                $detailList.Add($pair)
            }

            foreach ($pair in Get-XmlLeafPairs -Node $propertiesNode -SkipNames @('DisplayName', 'Explain', 'Name', 'PolicyName', 'Supported', 'SupportedOn', 'Title')) {
                $detailList.Add($pair)
            }
        }

        foreach ($pair in Get-XmlLeafPairs -Node $Node -SkipNames @('Action', 'DisplayName', 'Enabled', 'Explain', 'Name', 'PolicyName', 'State', 'Supported', 'SupportedOn', 'Title')) {
            $detailList.Add($pair)
        }

        foreach ($pair in Get-XmlNestedSummaryPairs -Node $Node) {
            $detailList.Add($pair)
        }

        $details = @($detailList | Where-Object { $_ } | Sort-Object -Unique)

        if ($details.Count -gt 0) {
            return ("{0} | {1} | {2}: {3} | {4}" -f $ScopeName, $ExtensionName, $settingType, $settingName, ($details -join '; '))
        }

        return ("{0} | {1} | {2}: {3}" -f $ScopeName, $ExtensionName, $settingType, $settingName)
    }

    function Format-ExtensionFallbackSummary {
        param(
            [Parameter(Mandatory = $true)]
            [System.Xml.XmlNode]$ExtensionNode,

            [Parameter(Mandatory = $true)]
            [string]$ScopeName,

            [Parameter(Mandatory = $true)]
            [string]$ExtensionName
        )

        $metadataNames = @(
            'Blocked',
            'Category',
            'DisplayName',
            'Enabled',
            'Explain',
            'Filters',
            'Name',
            'Properties',
            'Supported',
            'SupportedOn'
        )

        $childNames = @(
            Get-XmlElementChildren -Node $ExtensionNode |
                Where-Object { $metadataNames -inotcontains $_.LocalName } |
                ForEach-Object { $_.LocalName } |
                Sort-Object -Unique
        )

        if ($childNames.Count -gt 0) {
            return ("{0} | {1}: configured data exists, but it could not be flattened. XML node types found: {2}" -f $ScopeName, $ExtensionName, ($childNames -join ', '))
        }

        $attributePairs = @(Get-XmlAttributePairs -Node $ExtensionNode -SkipNames @('clsid', 'uid'))

        if ($attributePairs.Count -gt 0) {
            return ("{0} | {1}: configured data exists, but it could not be flattened. Extension attributes: {2}" -f $ScopeName, $ExtensionName, ($attributePairs -join '; '))
        }

        return $null
    }

    function Get-GPOConfiguredSettings {
        param(
            [Parameter(Mandatory = $true)]
            [guid]$GpoGuid,

            [Parameter(Mandatory = $true)]
            [string]$DomainName,

            [Parameter(Mandatory = $true)]
            [string]$Server,

            [string]$GpoName,

            [string]$SaveXmlFolder
        )

        try {
            $reportXmlText = Get-GPOReport `
                -Guid $GpoGuid `
                -Domain $DomainName `
                -Server $Server `
                -ReportType Xml `
                -ErrorAction Stop

            [xml]$reportXml = $reportXmlText

            if (-not [string]::IsNullOrWhiteSpace($SaveXmlFolder)) {
                if (-not (Test-Path -Path $SaveXmlFolder)) {
                    New-Item -ItemType Directory -Path $SaveXmlFolder -Force | Out-Null
                }

                $fileBaseName = if ([string]::IsNullOrWhiteSpace($GpoName)) {
                    $GpoGuid.ToString()
                }
                else {
                    "{0}_{1}" -f (ConvertTo-SafeFileName -Name $GpoName), $GpoGuid
                }

                $xmlPath = Join-Path -Path $SaveXmlFolder -ChildPath "$fileBaseName.xml"
                $reportXmlText | Out-File -FilePath $xmlPath -Encoding UTF8
            }
        }
        catch {
            return [PSCustomObject]@{
                Count    = 0
                Settings = "Could not read settings from GPO report XML: $($_.Exception.Message)"
            }
        }

        $settings = [System.Collections.Generic.List[string]]::new()

        foreach ($scopeName in @('Computer', 'User')) {
            $scopeNode = $reportXml.SelectSingleNode("/*[local-name()='GPO']/*[local-name()='$scopeName']")

            if (-not $scopeNode) {
                continue
            }

            $scopeLabel = $scopeName
            $enabledNode = $scopeNode.SelectSingleNode("./*[local-name()='Enabled']")

            if ($enabledNode -and $enabledNode.InnerText -ieq 'false') {
                $scopeLabel = "$scopeName (scope disabled)"
            }

            $extensionDataNodes = @($scopeNode.SelectNodes("./*[local-name()='ExtensionData']"))

            foreach ($extensionData in $extensionDataNodes) {
                $extensionName = Get-DirectChildText -Node $extensionData -Names @('Name')

                if (-not $extensionName) {
                    $extensionName = "Unknown extension"
                }

                $extensionNodes = @($extensionData.SelectNodes("./*[local-name()='Extension']"))

                foreach ($extensionNode in $extensionNodes) {
                    $settingsBeforeExtension = $settings.Count
                    $candidateNodes = Get-PolicySettingCandidateNodes -Root $extensionNode

                    foreach ($candidateNode in $candidateNodes) {
                        $settingLine = Format-PolicySetting `
                            -Node $candidateNode `
                            -ScopeName $scopeLabel `
                            -ExtensionName $extensionName

                        if ($settingLine) {
                            $settings.Add($settingLine)
                        }
                    }

                    if ($settings.Count -eq $settingsBeforeExtension) {
                        $fallbackLine = Format-ExtensionFallbackSummary `
                            -ExtensionNode $extensionNode `
                            -ScopeName $scopeLabel `
                            -ExtensionName $extensionName

                        if ($fallbackLine) {
                            $settings.Add($fallbackLine)
                        }
                    }
                }
            }
        }

        $uniqueSettings = @($settings | Where-Object { $_ } | Sort-Object -Unique)

        if ($uniqueSettings.Count -eq 0) {
            return [PSCustomObject]@{
                Count    = 0
                Settings = "No configured settings found in GPO report XML"
            }
        }

        return [PSCustomObject]@{
            Count    = $uniqueSettings.Count
            Settings = ($uniqueSettings -join "`n")
        }
    }

    function Get-WmiFilterQuery {
        param(
            [AllowNull()]
            [object]$WmiFilterObject
        )

        if (-not $WmiFilterObject) {
            return $null
        }

        $rawQuery = [string]$WmiFilterObject.'msWMI-Parm2'

        if ([string]::IsNullOrWhiteSpace($rawQuery)) {
            return $null
        }

        if ($rawQuery -match 'root\\[^;]+;(?<Query>.+)$') {
            return $Matches.Query.Trim(';')
        }

        return $rawQuery
    }

    function Invoke-ADOKGPOCollectionWithSettings {
        param(
            [Parameter(Mandatory = $true)]
            [string]$DomainName,

            [string]$DomainController,

            [string]$SaveXmlFolder
        )

        if ([string]::IsNullOrWhiteSpace($DomainController)) {
            $adDomain = Get-ADDomain -Identity $DomainName -ErrorAction Stop
            $pdc = [string]$adDomain.PDCEmulator
            $domainControllerSource = "PDC emulator"
        }
        else {
            $pdc = [string]$DomainController
            $adDomain = Get-ADDomain -Identity $DomainName -Server $pdc -ErrorAction Stop
            $domainControllerSource = "specified domain controller"
        }

        Write-Host "Using domain controller: $pdc ($domainControllerSource)" -ForegroundColor Yellow

        $allGPOs = @(
            Get-GPO `
                -All `
                -Domain $DomainName `
                -Server $pdc `
                -ErrorAction Stop
        )

        Write-Host "Found $($allGPOs.Count) GPO(s)." -ForegroundColor Yellow

        $linkMap = @{}

        foreach ($gpo in $allGPOs) {
            $linkMap[(Convert-GpoIdToKey -GpoId $gpo.Id)] = [System.Collections.Generic.List[string]]::new()
        }

        Write-Host "Reading domain root GPO links..." -ForegroundColor Yellow

        $domainObject = Get-ADObject `
            -Identity $adDomain.DistinguishedName `
            -Server $pdc `
            -Properties gPLink `
            -ErrorAction Stop

        foreach ($guid in Get-GuidFromGpLinkLocal -GpLink $domainObject.gPLink) {
            if ($linkMap.ContainsKey($guid)) {
                $linkMap[$guid].Add($adDomain.DistinguishedName)
            }
        }

        Write-Host "Reading OU GPO links..." -ForegroundColor Yellow

        $linkedOUs = @(
            Get-ADOrganizationalUnit `
                -LDAPFilter "(gPLink=*)" `
                -Server $pdc `
                -Properties gPLink `
                -ErrorAction Stop
        )

        foreach ($ou in $linkedOUs) {
            foreach ($guid in Get-GuidFromGpLinkLocal -GpLink $ou.gPLink) {
                if ($linkMap.ContainsKey($guid)) {
                    $linkMap[$guid].Add($ou.DistinguishedName)
                }
            }
        }

        Write-Host "Reading WMI filters..." -ForegroundColor Yellow

        $wmiFilters = @(
            Get-ADObject `
                -LDAPFilter "(objectClass=msWMI-Som)" `
                -Server $pdc `
                -Properties msWMI-Name, msWMI-Parm2 `
                -ErrorAction SilentlyContinue
        )

        $results = [System.Collections.Generic.List[PSCustomObject]]::new()
        $index = 0

        foreach ($gpo in $allGPOs) {
            $index++
            Write-Host ("Processing {0}/{1}: {2}" -f $index, $allGPOs.Count, $gpo.DisplayName) -ForegroundColor Gray

            $permissionRecords = @(
                Get-GPPermission `
                    -Guid $gpo.Id `
                    -All `
                    -DomainName $DomainName `
                    -Server $pdc `
                    -ErrorAction SilentlyContinue |
                    ForEach-Object {
                        [PSCustomObject]@{
                            Permission = "$($_.Trustee.Name), $($_.Trustee.SIDType), $($_.Permission), Denied: $($_.Denied)"
                            GPOApply   = if ($_.Permission -eq 'GpoApply') { $_.Trustee.Name } else { $null }
                        }
                    }
            )

            $gpoGuid = Convert-GpoIdToKey -GpoId $gpo.Id

            if ($linkMap.ContainsKey($gpoGuid)) {
                $links = @($linkMap[$gpoGuid] | Sort-Object -Unique)
            }
            else {
                $links = @()
            }

            $wmiFilterName = $null
            $wmiQuery = $null

            if ($gpo.WmiFilter) {
                $wmiFilterName = $gpo.WmiFilter.Name

                try {
                    $wmiFilterId = ($gpo.WmiFilter.Path -split '"')[1]

                    $matchedWmiFilter = $wmiFilters |
                        Where-Object {
                            $_.Name -eq $wmiFilterId -or
                            $_.'msWMI-Name' -eq $wmiFilterName
                        } |
                        Select-Object -First 1

                    $wmiQuery = Get-WmiFilterQuery -WmiFilterObject $matchedWmiFilter
                }
                catch {
                    $wmiQuery = "Could not read WMI query"
                }
            }

            $configuredSettings = Get-GPOConfiguredSettings `
                -GpoGuid $gpo.Id `
                -DomainName $DomainName `
                -Server $pdc `
                -GpoName $gpo.DisplayName `
                -SaveXmlFolder $SaveXmlFolder

            $results.Add([PSCustomObject]@{
                Domain                  = $DomainName
                DomainController        = $pdc
                GPOName                 = $gpo.DisplayName
                GPOId                   = $gpo.Id
                CreationTime            = $gpo.CreationTime
                ModificationTime        = $gpo.ModificationTime
                Links                   = if ($links.Count -gt 0) { $links -join "`n" } else { "Not linked" }
                ComputerSettings        = $gpo.Computer.Enabled
                UserSettings            = $gpo.User.Enabled
                GPOApply                = ($permissionRecords.GPOApply | Where-Object { $_ }) -join "`n"
                Permissions             = if ($permissionRecords.Count -gt 0) { $permissionRecords.Permission -join "`n" } else { "No permissions read" }
                WmiFilter               = $wmiFilterName
                WmiQuery                = $wmiQuery
                ConfiguredSettingsCount = $configuredSettings.Count
                ConfiguredSettings      = $configuredSettings.Settings
            })
        }

        return $results
    }

    $htmlHead = @'
<title>GPO Inventory With Settings Report</title>
<style>
body { font-family:"Segoe UI",Arial,sans-serif; font-size:13px; background:#f3f4f6; color:#22223b; margin:20px; }
h1 { color:#2a394f; border-bottom:2px solid #c9d6e3; padding-bottom:8px; }
h3 { color:#555; font-weight:normal; }
p { max-width:1200px; color:#555; line-height:1.45; }
table { border-collapse:collapse; width:100%; background:#fff; margin-top:16px; table-layout:auto; }
th { background:#2a394f; color:#fff; padding:10px; text-align:left; font-size:12px; position:sticky; top:0; }
td { border:1px solid #e1e5ee; padding:8px; vertical-align:top; font-size:12px; white-space:pre-line; }
tr:nth-child(even) td { background:#f8f8fc; }
tr:hover td { background:#eaf0fa; }
</style>
'@


    $htmlBody = @"
<h1>GPO Inventory With Settings Report</h1>
<h3>Domain: $DomainName &nbsp;|&nbsp; Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm')</h3>
<p>The ConfiguredSettings column lists settings configured inside each GPO from Get-GPOReport XML. It does not calculate RSOP, GPO precedence, inheritance, security filtering outcome, or WMI filter pass/fail outcome for a specific endpoint or user.</p>
"@


    Write-Host "Collecting GPO inventory for $DomainName..." -ForegroundColor Cyan

    $inventory = Invoke-ADOKGPOCollectionWithSettings -DomainName $DomainName -DomainController $DomainController -SaveXmlFolder $SaveGpoReportXmlFolder

    $outputFolder = Split-Path -Path $OutputPath -Parent

    if (-not [string]::IsNullOrWhiteSpace($outputFolder) -and -not (Test-Path -Path $outputFolder)) {
        New-Item -ItemType Directory -Path $outputFolder -Force | Out-Null
    }

    $inventory |
        Sort-Object GPOName |
        ConvertTo-Html `
            -Property Domain, DomainController, GPOName, GPOId, CreationTime, ModificationTime,
                      Links, ComputerSettings, UserSettings,
                      GPOApply, Permissions, WmiFilter, WmiQuery,
                      ConfiguredSettingsCount, ConfiguredSettings `
            -Head $htmlHead `
            -Body $htmlBody |
        Out-File -FilePath $OutputPath -Encoding UTF8

    Write-Host "Report written to $OutputPath" -ForegroundColor Green
    Write-Host "$($inventory.Count) GPO(s) inventoried." -ForegroundColor Cyan
}