Private/GroupPolicy/Collector.ps1
|
$script:PSIGpoCollector={ param([string]$Domain,[string]$HelperText,[bool]$ShowProgress=$false) $ErrorActionPreference='Stop';Set-StrictMode -Version 2.0 . ([scriptblock]::Create($HelperText)) function V($o,$p){if($null -ne $o -and $null -ne $o.PSObject.Properties[$p]){return $o.$p};return $null} function DateText($v){if($null -ne $v){return ([datetimeoffset]$v).ToString('o')};return $null} function Emit($Key,$Status,[object[]]$Rows,$Message='',$Id='', [long]$Ms=0){[pscustomobject]@{Kind='Section';Key=$Key;Status=$Status;Records=@($Rows);CollectedAt=[datetimeoffset]::Now.ToString('o');DurationMs=$Ms;ErrorId=$Id;ErrorMessage=$Message}} Import-Module ActiveDirectory -ErrorAction Stop;Import-Module GroupPolicy -ErrorAction Stop $root=Get-ADRootDSE -Server $Domain -ErrorAction Stop;$server=[string]$root.dnsHostName if(-not $server){throw 'Discovery did not return a pinned DC.'} $domainInfo=Get-ADDomain -Server $server -ErrorAction Stop if($domainInfo.DNSRoot -ine $Domain){throw 'Discovered DC does not serve the explicitly requested domain.'} $domainDN=[string]$domainInfo.DistinguishedName;$configDN=[string]$root.configurationNamingContext [pscustomobject]@{Kind='Context';Domain=$Domain;Server=$server;DomainDN=$domainDN;ConfigurationNC=$configDN;Forest=[string]$domainInfo.Forest} $ledger=New-Object collections.ArrayList;$gpos=@();$gpoError=$null;$watch=[diagnostics.stopwatch]::StartNew() try{$gpos=@(Get-GPO -All -Domain $Domain -Server $server -ErrorAction Stop);foreach($g in $gpos){if($g.DomainName -ine $Domain){throw 'A returned GPO is outside the selected domain.'};$null=[guid]$g.Id}}catch{$gpoError=$_;$gpos=@()} $watch.Stop() if($null -ne $gpoError){Emit 'gpos' 'Failed' @() $gpoError.Exception.Message $gpoError.FullyQualifiedErrorId $watch.ElapsedMilliseconds;Emit 'versions' 'NotAttempted' @() 'GPO enumeration failed.'} else{ Emit 'gpos' 'Succeeded' @($gpos|ForEach-Object{[pscustomobject]@{Name=$_.DisplayName;'GPO ID'=([guid]$_.Id).ToString();Domain=$_.DomainName;Owner=V $_ 'Owner';Status=[string]$_.GpoStatus;Created=DateText (V $_ 'CreationTime');Changed=DateText (V $_ 'ModificationTime');Description=V $_ 'Description'}}) '' '' $watch.ElapsedMilliseconds Emit 'versions' 'Succeeded' @($gpos|ForEach-Object{[pscustomobject]@{Name=$_.DisplayName;'GPO ID'=([guid]$_.Id).ToString();'User AD version'=V (V $_ 'User') 'DSVersion';'User SYSVOL version'=V (V $_ 'User') 'SysvolVersion';'Computer AD version'=V (V $_ 'Computer') 'DSVersion';'Computer SYSVOL version'=V (V $_ 'Computer') 'SysvolVersion'}}) } # Query each container source independently; preserve successful sources if another fails. $containerRows=New-Object collections.ArrayList;$linkRows=New-Object collections.ArrayList;$sourceFailures=0 foreach($source in @('Domain','OU','Site')){ $watch=[diagnostics.stopwatch]::StartNew();$status='Succeeded';$message='Collected';$id='';$retained=0 try{ $objects=switch($source){ 'Domain'{Get-ADObject -Identity $domainDN -Properties gPLink,gPOptions -Server $server -ErrorAction Stop} 'OU'{Get-ADObject -LDAPFilter '(objectClass=organizationalUnit)' -SearchBase $domainDN -SearchScope Subtree -Properties gPLink,gPOptions -Server $server -ErrorAction Stop} 'Site'{Get-ADObject -LDAPFilter '(objectClass=site)' -SearchBase ('CN=Sites,'+$configDN) -SearchScope OneLevel -Properties gPLink,gPOptions -Server $server -ErrorAction Stop} } foreach($o in @($objects)){ $dn=[string]$o.DistinguishedName if(($source -eq 'Domain' -and $dn -ine $domainDN) -or ($source -eq 'OU' -and -not $dn.EndsWith(','+$domainDN,[StringComparison]::OrdinalIgnoreCase)) -or ($source -eq 'Site' -and -not $dn.EndsWith(',CN=Sites,'+$configDN,[StringComparison]::OrdinalIgnoreCase))){throw 'Container scope guard rejected an unexpected DN.'} $allText=[string](V $o 'gPLink');$links=@(ConvertFrom-PSIGpoLinks $allText $o.Name $source $dn $domainDN) $options=V $o 'gPOptions' [void]$containerRows.Add([pscustomobject]@{Name=$o.Name;Type=$source;DN=$dn;'Block inheritance'=if($null -eq $options){$null}else{([int]$options -band 1) -ne 0};'Options raw'=$options;'Link entries observed'=[regex]::Matches($allText,'\[LDAP://','IgnoreCase').Count;'Selected-domain links'=$links.Count}) foreach($link in $links){[void]$linkRows.Add($link)};$retained++ } }catch{$status='Failed';$message=$_.Exception.Message;$id=$_.FullyQualifiedErrorId;$sourceFailures++} $watch.Stop();[void]$ledger.Add([pscustomobject]@{Name=$source+' link containers';'GPO ID'=$null;Target=$source;Operation='Link container source';Status=$status;'Record count'=$retained;'Duration ms'=$watch.ElapsedMilliseconds;'Error ID'=$id;Message=$message;'Collected at'=[datetimeoffset]::Now.ToString('o')}) } foreach($entry in @(@('containers',$containerRows),@('links',$linkRows))){Emit $entry[0] $(if($sourceFailures){'Partial'}else{'Succeeded'}) $entry[1].ToArray() $(if($sourceFailures){'One or more container sources failed; successful observations retained.'}else{'Declared container sources collected.'})} foreach($category in @('wmiFilters','wmiReferences')){ $watch=[diagnostics.stopwatch]::StartNew();$rows=@();$status='Succeeded';$message='Collected';$id='' try{ if($category -eq 'wmiFilters'){ $rows=@(Get-ADObject -LDAPFilter '(objectClass=msWMI-Som)' -SearchBase $domainDN -SearchScope Subtree -Properties msWMI-ID,msWMI-Name,msWMI-Author,msWMI-Parm1,msWMI-Parm2 -Server $server -ErrorAction Stop|ForEach-Object{ if(-not $_.DistinguishedName.EndsWith(','+$domainDN,[StringComparison]::OrdinalIgnoreCase)){throw 'WMI filter scope mismatch.'} [pscustomobject]@{Name=V $_ 'msWMI-Name';'Filter ID'=([string](V $_ 'msWMI-ID')).Trim('{}').ToLowerInvariant();Author=V $_ 'msWMI-Author';Description=V $_ 'msWMI-Parm1';'Query definition raw'=V $_ 'msWMI-Parm2';DN=$_.DistinguishedName} }) }else{ $rows=@(Get-ADObject -LDAPFilter '(objectClass=groupPolicyContainer)' -SearchBase ('CN=Policies,CN=System,'+$domainDN) -SearchScope OneLevel -Properties displayName,gPCWQLFilter -Server $server -ErrorAction Stop|ForEach-Object{ if(-not $_.DistinguishedName.EndsWith(',CN=Policies,CN=System,'+$domainDN,[StringComparison]::OrdinalIgnoreCase)){throw 'GPC scope mismatch.'} $ref=[string](V $_ 'gPCWQLFilter');$filterId=$null;$filterDomain=$null;$m=[regex]::Match($ref,'^\[(?<domain>[^;]+);\{(?<id>[0-9a-f-]{36})\};\d+\]$','IgnoreCase') if($m.Success){$filterId=([guid]$m.Groups['id'].Value).ToString();$filterDomain=$m.Groups['domain'].Value} [pscustomobject]@{Name=V $_ 'displayName';'GPO ID'=([guid]$_.Name.Trim('{}')).ToString();'Filter reference raw'=if($ref){$ref}else{$null};'Filter ID'=$filterId;'Filter domain'=$filterDomain} }) } }catch{$status='Failed';$rows=@();$message=$_.Exception.Message;$id=$_.FullyQualifiedErrorId} $watch.Stop();Emit $category $status $rows $message $id $watch.ElapsedMilliseconds } foreach($category in @('permissions','settings')){ if($null -ne $gpoError){Emit $category 'NotAttempted' @() 'GPO enumeration failed.';continue} $rows=New-Object collections.ArrayList;$failures=0 foreach($g in $gpos){ if($ShowProgress){[pscustomobject]@{Kind='Progress';Stage=('Reading GPO '+$category);Target=$g.DisplayName}} $guid=([guid]$g.Id).ToString();$status='Succeeded';$message='Collected';$id='';$count=0;$watch=[diagnostics.stopwatch]::StartNew() try{ $records=if($category -eq 'permissions'){ @(Get-GPPermission -Guid $guid -All -Domain $Domain -Server $server -ErrorAction Stop|ForEach-Object{ $t=V $_ 'Trustee';$sid=V $t 'Sid';$sidText=if($null -ne (V $sid 'Value')){[string]$sid.Value}elseif($null -ne $sid){[string]$sid}else{$null} [pscustomobject]@{Name=$g.DisplayName;'GPO ID'=$guid;Trustee=V $t 'Name';'Trustee domain'=V $t 'Domain';'Trustee SID'=$sidText;'Trustee type'=V $_ 'TrusteeType';Permission=[string]$_.Permission;Inherited=V $_ 'Inherited'} }) }else{@(ConvertFrom-PSIGpoXml (Get-GPOReport -Guid $guid -Domain $Domain -Server $server -ReportType Xml -ErrorAction Stop) $guid $g.DisplayName)} foreach($row in $records){[void]$rows.Add($row)};$count=@($records).Count }catch{$status='Failed';$message=$_.Exception.Message;$id=$_.FullyQualifiedErrorId;$failures++} $watch.Stop();[void]$ledger.Add([pscustomobject]@{Name=$g.DisplayName;'GPO ID'=$guid;Target=$guid;Operation=$category;Status=$status;'Record count'=$count;'Duration ms'=$watch.ElapsedMilliseconds;'Error ID'=$id;Message=$message;'Collected at'=[datetimeoffset]::Now.ToString('o')}) } Emit $category $(if($failures){'Partial'}else{'Succeeded'}) $rows.ToArray() $(if($failures){"$failures GPO queries failed; successful results retained."}else{'All enumerated GPO queries completed.'}) } Emit 'objectQueries' 'Succeeded' $ledger.ToArray() 'Ledger captured; inspect each outcome.' } function Invoke-PSIGpoCollection([string]$Domain,[int]$TimeoutSeconds,[scriptblock]$Worker,[string]$HelperText){ $job=$null;$context=$null;$envelopes=New-Object collections.ArrayList;$seen=@{};$failure='';$timedOut=$false;$watch=[diagnostics.stopwatch]::StartNew() try{ $showActivity=$false;if(Get-Command Get-PSIInventoryDisplayState -ErrorAction SilentlyContinue){$displayState=Get-PSIInventoryDisplayState;$showActivity=($null -ne $displayState -and -not $displayState.Quiet)} $job=Start-Job -ScriptBlock $Worker -ArgumentList $Domain,$HelperText,$showActivity -ErrorAction Stop do{ $errors=@();$batch=@(Receive-Job $job -ErrorAction SilentlyContinue -ErrorVariable errors) foreach($e in $batch){if($e.Kind -eq 'Progress'){continue};if($e.Kind -eq 'Context'){if($null -ne $context){throw 'Duplicate context.'};$context=$e}elseif($e.Kind -eq 'Section'){if($seen.ContainsKey([string]$e.Key)){throw 'Duplicate category.'};$seen[[string]$e.Key]=$true;[void]$envelopes.Add($e)}else{throw 'Unexpected collection output.'}} if(Get-Command Receive-PSIInventoryDisplay -ErrorAction SilentlyContinue){Receive-PSIInventoryDisplay -Items $batch -Total 9} if($errors.Count){$failure=($errors|ForEach-Object{$_.Exception.Message}) -join '; '} if($job.State -in @('Completed','Failed','Stopped')){ $lastErrors=@();$last=@(Receive-Job $job -ErrorAction SilentlyContinue -ErrorVariable lastErrors) foreach($e in $last){if($e.Kind -eq 'Progress'){continue};if($e.Kind -eq 'Context'){if($null -ne $context){throw 'Duplicate context.'};$context=$e}elseif($e.Kind -eq 'Section'){if($seen.ContainsKey([string]$e.Key)){throw 'Duplicate category.'};$seen[[string]$e.Key]=$true;[void]$envelopes.Add($e)}else{throw 'Unexpected collection output.'}} if(Get-Command Receive-PSIInventoryDisplay -ErrorAction SilentlyContinue){Receive-PSIInventoryDisplay -Items $last -Total 9} if($lastErrors.Count){$failure=($lastErrors|ForEach-Object{$_.Exception.Message}) -join '; '} break } if($watch.Elapsed.TotalSeconds -ge $TimeoutSeconds){$timedOut=$true;Stop-Job $job -ErrorAction SilentlyContinue;continue} Start-Sleep -Milliseconds 200 }while($true) if($null -eq $context){throw ('Domain collection did not establish scope. '+$failure)} if($context.Domain -ine $Domain){throw 'Returned context differs from requested domain.'} foreach($key in (Get-PSIGpoSchema).Keys){if($key -ne 'collection' -and -not $seen.ContainsKey($key)){[void]$envelopes.Add([pscustomobject]@{Kind='Section';Key=$key;Status=if($timedOut){'TimedOut'}else{'Failed'};Records=@();CollectedAt=[datetimeoffset]::Now.ToString('o');DurationMs=$watch.ElapsedMilliseconds;ErrorId='GroupPolicy.Incomplete';ErrorMessage=if($timedOut){'Domain worker exceeded its overall time budget.'}elseif($failure){$failure}else{'Worker did not return this category.'}})}} [pscustomobject]@{Context=$context;Envelopes=$envelopes.ToArray()} }finally{if($null -ne $job){if($job.State -notin @('Completed','Failed','Stopped')){Stop-Job $job -ErrorAction SilentlyContinue};Remove-Job $job -Force -ErrorAction SilentlyContinue}} } |