Public/Search-GPO.ps1

function Search-GPO {
    <#
    .SYNOPSIS
        Searches GPO reports and SYSVOL scripts for a literal string.
 
    .DESCRIPTION
        Scans GPO XML metadata and Startup, Shutdown, Logon, and Logoff scripts.
        Emits structured result objects. Use -ShowDetails to select the detailed
        default display view without changing the returned object contract.
 
    .PARAMETER SearchTerm
        The literal string to search for within GPO reports and script content.
 
    .PARAMETER ShowDetails
        Selects the detailed default display view for returned result objects.
 
    .EXAMPLE
        Search-GPO -SearchTerm "Printer"
 
    .EXAMPLE
        Search-GPO "Deploy" -ShowDetails
 
    .RELATEDLINKS
        https://deepwiki.com/DailenG/Search-GPO
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [string]$SearchTerm,

        [switch]$ShowDetails
    )

    begin {
        try {
            Import-Module -Name GroupPolicy -ErrorAction Stop
        }
        catch {
            throw "Search-GPO requires the GroupPolicy module. Install RSAT Group Policy Management tools. $($_.Exception.Message)"
        }
    }

    process {
        if ([string]::IsNullOrWhiteSpace($SearchTerm)) {
            try {
                $SearchTerm = Read-Host "Enter the server name or string to search for"
            }
            catch {
                throw "Could not read input. Run with: Search-GPO -SearchTerm 'Name'. $($_.Exception.Message)"
            }
        }

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

        $DomainDNS = $env:USERDNSDOMAIN
        if ([string]::IsNullOrWhiteSpace($DomainDNS)) {
            throw "USERDNSDOMAIN is not set. Run Search-GPO from a domain-joined session."
        }

        $EscapedSearchTerm = [regex]::Escape($SearchTerm)
        $SysvolBase = "\\$DomainDNS\SYSVOL\$DomainDNS\Policies"
        $Timer = [System.Diagnostics.Stopwatch]::StartNew()
        $Results = [System.Collections.Generic.List[object]]::new()
        $Failures = [System.Collections.Generic.List[object]]::new()

        Write-Information -MessageData "Initializing GPO scan for: $SearchTerm" -InformationAction Continue
        Write-Verbose "Targeting Domain: $DomainDNS"
        Write-Verbose "SYSVOL Root: $SysvolBase"

        $AllGpos = @(Get-GPO -All -ErrorAction Stop)
        $TotalCount = $AllGpos.Count

        for ($Index = 0; $Index -lt $TotalCount; $Index++) {
            $Gpo = $AllGpos[$Index]
            $Current = $Index + 1
            $Percent = [math]::Round(($Current / $TotalCount) * 100, 0)
            $Timestamp = Get-Date -Format "HH:mm:ss"
            $GpoName = $Gpo.DisplayName
            $GpoGuid = ([guid]$Gpo.Id).ToString('B').ToUpperInvariant()
            $PolicyPath = Join-Path -Path $SysvolBase -ChildPath $GpoGuid
            $MatchSource = [System.Collections.Generic.List[string]]::new()
            $MatchEvidence = [System.Collections.Generic.List[string]]::new()
            $LinkPaths = "--- NONE ---"

            Write-Progress -Activity "Scanning GPOs (Elapsed: $($Timer.Elapsed.ToString('mm\:ss')))" `
                -Status "[$Timestamp] Analyzing [$Current/$TotalCount]: $GpoName" `
                -PercentComplete $Percent
            Write-Verbose "[$Timestamp] START: $GpoName [ID: $($Gpo.Id)]"

            try {
                Write-Verbose " -> Metadata: analyzing XML settings."
                [xml]$GpoXml = Get-GPOReport -Guid $Gpo.Id -ReportType Xml -ErrorAction Stop
                $XmlText = $GpoXml.OuterXml

                if ($XmlText.IndexOf($SearchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
                    $MatchSource.Add("GPO Settings")
                    Write-Verbose " Match found in metadata XML."
                    if ($XmlText -match "([^>]{0,50}$EscapedSearchTerm[^<]{0,50})") {
                        $MatchEvidence.Add("XML Snippet: ...$($Matches[1])...")
                    }
                }

                $ActiveLinks = @($GpoXml.GPO.LinksTo) | Where-Object { $_.Enabled -eq "true" }
                if ($ActiveLinks) {
                    $LinkPaths = $ActiveLinks.SOMPath -join "; "
                }

                Write-Verbose " -> Link status: $(if ($ActiveLinks) { 'Linked' } else { 'Unlinked' })"
            }
            catch {
                $Failures.Add([pscustomobject]@{
                        GPOName = $GpoName
                        Stage   = "GPO Settings"
                        Path    = $null
                        Error   = $_.Exception.Message
                    })
                $LinkPaths = "--- ERROR ---"
                Write-Warning "Search-GPO could not read metadata for GPO '$GpoName': $($_.Exception.Message)"
            }

            $ScriptFolders = @(
                [pscustomobject]@{ Label = "Machine\Scripts\Startup"; Path = Join-Path -Path $PolicyPath -ChildPath "Machine\Scripts\Startup" }
                [pscustomobject]@{ Label = "Machine\Scripts\Shutdown"; Path = Join-Path -Path $PolicyPath -ChildPath "Machine\Scripts\Shutdown" }
                [pscustomobject]@{ Label = "User\Scripts\Logon"; Path = Join-Path -Path $PolicyPath -ChildPath "User\Scripts\Logon" }
                [pscustomobject]@{ Label = "User\Scripts\Logoff"; Path = Join-Path -Path $PolicyPath -ChildPath "User\Scripts\Logoff" }
            )

            foreach ($Folder in $ScriptFolders) {
                try {
                    if (-not (Test-Path -LiteralPath $Folder.Path -PathType Container)) {
                        continue
                    }

                    Write-Verbose " -> Script folder: $($Folder.Label)"
                    $FileMatches = @(Get-ChildItem -LiteralPath $Folder.Path -File -Recurse -ErrorAction Stop |
                            Select-String -Pattern $EscapedSearchTerm -ErrorAction Stop)

                    if ($FileMatches.Count -gt 0) {
                        if ($MatchSource -notcontains "Script Code") {
                            $MatchSource.Add("Script Code")
                        }

                        Write-Verbose " Match found in script content."
                        foreach ($FileMatch in $FileMatches) {
                            $MatchEvidence.Add("File ($($FileMatch.Filename)) line $($FileMatch.LineNumber): $($FileMatch.Line.Trim())")
                        }
                    }
                }
                catch {
                    $Failures.Add([pscustomobject]@{
                            GPOName = $GpoName
                            Stage   = "Script Code"
                            Path    = $Folder.Path
                            Error   = $_.Exception.Message
                        })
                    Write-Warning "Search-GPO could not scan '$($Folder.Label)' for GPO '$GpoName': $($_.Exception.Message)"
                }
            }

            if ($MatchSource.Count -gt 0) {
                $Result = [pscustomobject]@{
                    GPOName      = $GpoName
                    IsLinked     = $LinkPaths -notin "--- NONE ---", "--- ERROR ---"
                    Source       = $MatchSource -join ", "
                    MatchDetails = $MatchEvidence -join " | "
                    LastModified = $Gpo.ModificationTime
                    LinkPaths    = $LinkPaths
                }
                $Result.PSObject.TypeNames.Insert(0, "SearchGPO.Result")
                if ($ShowDetails) {
                    $Result.PSObject.TypeNames.Insert(0, "SearchGPO.Result.Detail")
                }
                $Results.Add($Result)
            }

            Write-Verbose "[$Timestamp] FINISH: $GpoName"
        }

        $Timer.Stop()
        Write-Progress -Activity "Scanning GPOs" -Completed

        $FailureDetails = $Failures.ToArray()
        foreach ($Result in $Results) {
            $Result | Add-Member -NotePropertyName ScanFailureCount -NotePropertyValue $FailureDetails.Count
            $Result | Add-Member -NotePropertyName ScanFailures -NotePropertyValue $FailureDetails
        }

        if ($FailureDetails.Count -gt 0) {
            Write-Warning "Search-GPO completed with $($FailureDetails.Count) failed scan step(s). See preceding warnings and the ScanFailures property on each result."
        }

        if ($Results.Count -gt 0) {
            Write-Information -MessageData "Scan complete: found $($Results.Count) match(es) in $($Timer.Elapsed.ToString('mm\:ss'))." -InformationAction Continue
            $Results
        }
        else {
            Write-Information -MessageData "No matches found for '$SearchTerm' across $TotalCount GPOs." -InformationAction Continue
        }
    }
}