Private/Groups/Collector.ps1
|
$script:PSIGroupCollector={ param([string]$Domain,[string]$HelperText,[bool]$ShowProgress=$false) $ErrorActionPreference='Stop';Set-StrictMode -Version 2.0;. ([scriptblock]::Create($HelperText)) function DateText($v){if($null -ne $v){return ([datetimeoffset]$v).ToString('o')};return $null} Import-Module ActiveDirectory -ErrorAction Stop $root=Get-ADRootDSE -Server $Domain -ErrorAction Stop;$server=[string]$root.dnsHostName if(-not $server){throw 'No pinned DC returned.'} $domainInfo=Get-ADDomain -Server $server -ErrorAction Stop if($domainInfo.DNSRoot -ine $Domain){throw 'Discovered DC is outside the requested domain.'} $domainDN=[string]$domainInfo.DistinguishedName [pscustomobject]@{Kind='Context';Domain=$Domain;Server=$server;DomainDN=$domainDN;Forest=[string]$domainInfo.Forest;ConfigurationNC=[string]$root.configurationNamingContext} $watch=[diagnostics.stopwatch]::StartNew();$catalog=New-Object collections.ArrayList;$errorRecord=$null;$groupByDN=@{};$ids=@{} try{ Get-ADGroup -Filter * -SearchBase $domainDN -SearchScope Subtree -Properties description,managedBy,whenCreated,whenChanged -Server $server -ErrorAction Stop|ForEach-Object{ $id=([guid]$_.ObjectGUID).ToString();$dn=[string]$_.DistinguishedName if((Get-PSIGroupDomainDN $dn) -ine $domainDN -or $ids.ContainsKey($id) -or $groupByDN.ContainsKey($dn)){throw 'Duplicate/out-of-domain group.'} $row=[pscustomobject]@{Id=$id;Name=$_.Name;DN=$dn;Sam=Get-PSIGroupValue $_ 'SamAccountName';Sid=[string]$_.SID;Scope=[string]$_.GroupScope;Category=[string]$_.GroupCategory;Description=Get-PSIGroupValue $_ 'description';ManagedBy=Get-PSIGroupValue $_ 'managedBy';Created=DateText (Get-PSIGroupValue $_ 'whenCreated');Changed=DateText (Get-PSIGroupValue $_ 'whenChanged')} [void]$catalog.Add($row);$groupByDN[$dn]=$row;$ids[$id]=$true } }catch{$errorRecord=$_;$catalog.Clear()} $watch.Stop() [pscustomobject]@{Kind='Section';Key='groups';Status=if($null -ne $errorRecord){'Failed'}else{'Succeeded'};Records=$catalog.ToArray();CollectedAt=[datetimeoffset]::Now.ToString('o');DurationMs=$watch.ElapsedMilliseconds;ErrorId=if($null -ne $errorRecord){$errorRecord.FullyQualifiedErrorId}else{''};ErrorMessage=if($null -ne $errorRecord){$errorRecord.Exception.Message}else{'Selected-domain group metadata collected.'}} if($null -ne $errorRecord){return} $cache=@{} foreach($g in $catalog){ if($ShowProgress){[pscustomobject]@{Kind='Progress';Stage='Reading group membership';Target=$g.Name}} $watch=[diagnostics.stopwatch]::StartNew();$rows=New-Object collections.ArrayList;$lookups=New-Object collections.ArrayList;$status='Succeeded';$errorId='';$message='Direct member attribute references captured; primary-group membership is excluded.' try{ $members=@(Get-PSIGroupMemberDNs $server $g.DN) foreach($memberDN in $members){ if(-not $cache.ContainsKey($memberDN)){ $resolved=[pscustomobject]@{DN=$memberDN;Name=$null;Id=$null;Sid=$null;Type='Unknown';Resolution='NotAttempted';Message='Reference outside selected naming context; not dereferenced.'} if($groupByDN.ContainsKey($memberDN)){$known=$groupByDN[$memberDN];$resolved.Name=$known.Name;$resolved.Id=$known.Id;$resolved.Sid=$known.Sid;$resolved.Type='group';$resolved.Resolution='Succeeded';$resolved.Message='Resolved from selected-domain group catalog.'} elseif((Get-PSIGroupDomainDN $memberDN) -ieq $domainDN){ try{$o=Get-ADObject -Identity $memberDN -Properties objectSid -Server $server -ErrorAction Stop if([string]$o.DistinguishedName -ine $memberDN){throw 'Member lookup returned a different DN.'} $resolved.Name=$o.Name;$resolved.Id=([guid]$o.ObjectGUID).ToString();$resolved.Sid=if($null -ne (Get-PSIGroupValue $o 'objectSid')){[string]$o.objectSid}else{$null};$resolved.Type=[string]$o.ObjectClass;$resolved.Resolution='Succeeded';$resolved.Message='Resolved through pinned selected-domain DC.' }catch{$resolved.Resolution='Failed';$resolved.Message=$_.Exception.Message} } $cache[$memberDN]=$resolved } $resolved=$cache[$memberDN];[void]$rows.Add($resolved);[void]$lookups.Add([pscustomobject]@{Target=$memberDN;Status=$resolved.Resolution;Message=$resolved.Message}) } }catch{$status='Failed';$rows.Clear();$lookups.Clear();$message=$_.Exception.Message;$errorId=$_.FullyQualifiedErrorId} $watch.Stop() [pscustomobject]@{Kind='Section';Key='members:'+$g.Id;Status=$status;Records=$rows.ToArray();Lookups=$lookups.ToArray();CollectedAt=[datetimeoffset]::Now.ToString('o');DurationMs=$watch.ElapsedMilliseconds;ErrorId=$errorId;ErrorMessage=$message} } } function Invoke-PSIGroupCollection([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 -1} 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 -1} 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-PSIGroupQuerySchema).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='Groups.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();TimedOut=$timedOut} }finally{if($null -ne $job){if($job.State -notin @('Completed','Failed','Stopped')){Stop-Job $job -ErrorAction SilentlyContinue};Remove-Job $job -Force -ErrorAction SilentlyContinue}} } |