Scripts/Update-GPOCache.ps1

function Update-GPOCache {

    [CmdletBinding()]
    param(
        [string]$Domain = $env:USERDNSDOMAIN,

        [string]$CachePath = "$env:ProgramData\GPOCache",

        [ValidateRange(1,50)]
        [int]$Threads = 10
    )

    Import-Module GroupPolicy -ErrorAction Stop

    $ManifestFile =
        Join-Path $CachePath "gpos.json"

    $IndexFile =
        Join-Path $CachePath "SearchIndex.json"

    $CacheFile =
        Join-Path $CachePath "cache.json"

    if (
        !(Test-Path $ManifestFile) -or
        !(Test-Path $IndexFile)
    ) {

        Write-Warning "Cache missing. Building full cache."

        New-GPOCache `
            -Domain $Domain `
            -CachePath $CachePath `
            -Threads $Threads

        return
    }

    #
    # Existing data
    #
    $OldManifest =
        Get-Content `
            $ManifestFile `
            -Raw |
        ConvertFrom-Json

    $SearchIndex =
        Get-Content `
            $IndexFile `
            -Raw |
        ConvertFrom-Json

    #
    # Current domain data
    #
    $CurrentGPOs =
        Get-GPO -All -Domain $Domain

    #
    # Fast lookups
    #
    $OldLookup = @{}

    foreach ($Entry in $OldManifest) {

        $OldLookup[$Entry.Guid] = $Entry
    }

    $ChangedGPOs = @()

    #
    # Detect New/Changed
    #
    foreach ($GPO in $CurrentGPOs) {

        $Guid = $GPO.Id.Guid

        if (
            !$OldLookup.ContainsKey($Guid)
        ) {

            $ChangedGPOs += $GPO
            continue
        }

        if (
            [datetime]$OldLookup[$Guid].ModificationTime `
            -lt `
            $GPO.ModificationTime
        ) {

            $ChangedGPOs += $GPO
        }
    }

    #
    # Detect Deleted
    #
    $CurrentGuids =
        $CurrentGPOs.Id.Guid

    $DeletedGuids =
        $OldManifest |
        Where-Object {
            $_.Guid -notin $CurrentGuids
        } |
        Select-Object -ExpandProperty Guid

    Write-Host ""
    Write-Host "Changes detected:" `
        -ForegroundColor Cyan

    Write-Host " New/Changed : $($ChangedGPOs.Count)"
    Write-Host " Deleted : $($DeletedGuids.Count)"
    Write-Host ""

    #
    # Remove deleted XML files
    #
    foreach ($Guid in $DeletedGuids) {

        $XmlFile =
            Join-Path `
            $CachePath `
            "$Guid.xml"

        if (
            Test-Path $XmlFile
        ) {
            Remove-Item `
                $XmlFile `
                -Force
        }
    }

    #
    # Remove deleted entries
    #
    if ($DeletedGuids.Count) {

        $SearchIndex =
            $SearchIndex |
            Where-Object {
                $_.Guid -notin $DeletedGuids
            }
    }

    #
    # Nothing changed?
    #
    if (
        $ChangedGPOs.Count -eq 0 `
        -and `
        $DeletedGuids.Count -eq 0
    ) {

        Write-Host `
            "Cache already up to date." `
            -ForegroundColor Green

        return
    }

    #
    # Runspace Pool
    #
    $Pool =
        [RunspaceFactory]::CreateRunspacePool(
            1,
            $Threads
        )

    $Pool.Open()

    $Jobs = @()

    foreach (
        $GPO in
        $ChangedGPOs
    ) {

        $XmlFile =
            Join-Path `
            $CachePath `
            "$($GPO.Id.Guid).xml"

        $PS =
            [PowerShell]::Create()

        $PS.RunspacePool = $Pool

        [void]$PS.AddScript({

            param(
                $Guid,
                $XmlFile
            )

            Import-Module GroupPolicy

            Get-GPOReport `
                -Guid $Guid `
                -ReportType Xml `
                -Path $XmlFile
        })

        [void]$PS.AddArgument(
            $GPO.Id
        )

        [void]$PS.AddArgument(
            $XmlFile
        )

        $Jobs += [PSCustomObject]@{
            PS     = $PS
            Handle = $PS.BeginInvoke()
        }
    }

    #
    # Progress
    #
    do {

        $Completed =
            (
                $Jobs |
                Where-Object {
                    $_.Handle.IsCompleted
                }
            ).Count

        Write-Progress `
            -Activity "Updating GPO Cache" `
            -Status "$Completed of $($Jobs.Count)" `
            -PercentComplete (
                ($Completed / [Math]::Max($Jobs.Count,1)) * 100
            )

        Start-Sleep -Milliseconds 500

    } until (
        $Completed -eq $Jobs.Count
    )

    #
    # Cleanup
    #
    foreach ($Job in $Jobs) {

        try {

            $Job.PS.EndInvoke(
                $Job.Handle
            )
        }
        finally {

            $Job.PS.Dispose()
        }
    }

    $Pool.Close()
    $Pool.Dispose()

    #
    # Remove old index entries
    #
    $ChangedIds =
        $ChangedGPOs.Id.Guid

    $SearchIndex =
        $SearchIndex |
        Where-Object {
            $_.Guid -notin $ChangedIds
        }

    #
    # Rebuild only changed entries
    #
    $IgnoredNodes = @(
        "ReadTime",
        "Changed",
        "Identifier",
        "FilterDataAvailable",
        "VersionDirectory"
    )

    foreach (
        $GPO in
        $ChangedGPOs
    ) {

        $XmlFile =
            Join-Path `
            $CachePath `
            "$($GPO.Id.Guid).xml"

        try {

            [xml]$Xml =
                Get-Content `
                $XmlFile `
                -Raw

            #
            # GPO name
            #
            $SearchIndex +=
                [PSCustomObject]@{
                    Guid        = $GPO.Id.Guid
                    DisplayName = $GPO.DisplayName
                    MatchType   = "GPOName"
                    Value       = $GPO.DisplayName
                }

            foreach (
                $Node in
                $Xml.SelectNodes("//*")
            ) {

                if (
                    $Node.Name -in $IgnoredNodes
                ) {
                    continue
                }

                if (
                    !$Node.InnerText
                ) {
                    continue
                }

                $Value =
                    $Node.InnerText.Trim()

                if (
                    [string]::IsNullOrWhiteSpace(
                        $Value
                    )
                ) {
                    continue
                }

                $SearchIndex +=
                    [PSCustomObject]@{
                        Guid        = $GPO.Id.Guid
                        DisplayName = $GPO.DisplayName
                        MatchType   = $Node.Name
                        Value       = $Value
                    }
            }
        }
        catch {

            Write-Warning `
                "Failed indexing $($GPO.DisplayName)"
        }
    }

    #
    # Save Search Index
    #
    $SearchIndex |
        ConvertTo-Json -Depth 8 |
        Set-Content `
            $IndexFile `
            -Encoding UTF8

    #
    # Save Manifest
    #
    $NewManifest =
        foreach (
            $GPO in
            $CurrentGPOs
        ) {

            [PSCustomObject]@{
                Guid             = $GPO.Id.Guid
                DisplayName      = $GPO.DisplayName
                ModificationTime = $GPO.ModificationTime
            }
        }

    $NewManifest |
        ConvertTo-Json -Depth 5 |
        Set-Content `
            $ManifestFile `
            -Encoding UTF8

    #
    # Update metadata
    #
    $Metadata = [PSCustomObject]@{

        CacheVersion = 1
        CreatedDate  = Get-Date
        Domain       = $Domain
        GPOCount     = $CurrentGPOs.Count
        IndexCount   = $SearchIndex.Count
    }

    $Metadata |
        ConvertTo-Json |
        Set-Content `
            $CacheFile `
            -Encoding UTF8

    Write-Progress `
        -Activity "Updating GPO Cache" `
        -Completed

    Write-Host ""
    Write-Host `
        "Incremental cache update completed." `
        -ForegroundColor Green
}