Private/OrganizationalUnits/Collector.ps1

$script:PSIOuCollector={
 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 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
 $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 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();$ous=@();$errorRecord=$null
 try{
  $ous=@(Get-ADOrganizationalUnit -Filter * -SearchBase $domainDN -SearchScope Subtree -Properties canonicalName,description,managedBy,ProtectedFromAccidentalDeletion,whenCreated,whenChanged,gPLink,gPOptions -Server $server -ErrorAction Stop)
  $ids=@{};$dns=@{}
  foreach($ou in $ous){$oid=([guid]$ou.ObjectGUID).ToString();$odn=[string]$ou.DistinguishedName;if(-not $odn.EndsWith(','+$domainDN,[StringComparison]::OrdinalIgnoreCase)){throw 'OU scope mismatch.'};if($ids.ContainsKey($oid) -or $dns.ContainsKey($odn)){throw 'Duplicate OU identity.'};$ids[$oid]=$true;$dns[$odn]=$true}
 }catch{$errorRecord=$_;$ous=@()}
 $watch.Stop()
 if($null -ne $errorRecord){Emit 'ous' 'Failed' @() $errorRecord.Exception.Message $errorRecord.FullyQualifiedErrorId $watch.ElapsedMilliseconds;foreach($key in @('counts','links','inheritance','delegations','objectQueries')){Emit $key 'NotAttempted' @() 'OU enumeration failed.'};return}
 Emit 'ous' 'Succeeded' @($ous|ForEach-Object{[pscustomobject]@{Name=$_.Name;'OU ID'=([guid]$_.ObjectGUID).ToString();DN=$_.DistinguishedName;'Parent DN'=Get-PSIOuParentDN $_.DistinguishedName;'Canonical path'=V $_ 'canonicalName';Description=V $_ 'description';'Managed by'=V $_ 'managedBy';'Protected from accidental deletion'=V $_ 'ProtectedFromAccidentalDeletion';Created=if($null -ne (V $_ 'whenCreated')){([datetimeoffset]$_.whenCreated).ToString('o')}else{$null};Changed=if($null -ne (V $_ 'whenChanged')){([datetimeoffset]$_.whenChanged).ToString('o')}else{$null}}}) '' '' $watch.ElapsedMilliseconds
 $ledger=New-Object collections.ArrayList;$countMap=@{}
 foreach($ou in $ous){$row=[ordered]@{Name=$ou.Name;'OU ID'=([guid]$ou.ObjectGUID).ToString();DN=$ou.DistinguishedName;'Count status'='Collected'};foreach($kind in @('users','computers','groups','OUs')){foreach($mode in @('Direct','Descendant')){$row[$mode+' '+$kind]=0}};$countMap[[string]$ou.DistinguishedName]=[pscustomobject]$row}
 $watch=[diagnostics.stopwatch]::StartNew();$countError=$null
 try{
  if($ous.Count){
   Get-ADObject -LDAPFilter '(|(objectCategory=person)(objectClass=computer)(objectClass=group)(objectClass=organizationalUnit))' -SearchBase $domainDN -SearchScope Subtree -Server $server -ErrorAction Stop|ForEach-Object{
    $odn=[string]$_.DistinguishedName;if(-not $odn.EndsWith(','+$domainDN,[StringComparison]::OrdinalIgnoreCase)){throw 'Count scan returned an out-of-domain object.'}
    $kind=switch([string]$_.ObjectClass){'user'{'users'}'computer'{'computers'}'group'{'groups'}'organizationalUnit'{'OUs'}default{$null}}
    if($null -ne $kind){
     $parent=Get-PSIOuParentDN $odn;$direct=$parent
     while($parent -and $parent -ine $domainDN){if($countMap.ContainsKey($parent)){$countMap[$parent].('Descendant '+$kind)++;if($parent -ieq $direct){$countMap[$parent].('Direct '+$kind)++}};$parent=Get-PSIOuParentDN $parent}
    }
   }
  }
 }catch{$countError=$_}
 $watch.Stop()
 if($null -ne $countError){Emit 'counts' 'Failed' @() $countError.Exception.Message $countError.FullyQualifiedErrorId $watch.ElapsedMilliseconds}
 else{Emit 'counts' 'Succeeded' @($ous|ForEach-Object{$countMap[[string]$_.DistinguishedName]}) 'Direct is one level; descendant includes all depths, excluding the OU itself.' '' $watch.ElapsedMilliseconds}
 $linkRows=New-Object collections.ArrayList;$inheritRows=New-Object collections.ArrayList;$aceRows=New-Object collections.ArrayList;$linkFailures=0;$aclFailures=0
 foreach($ou in $ous){
  if($ShowProgress){[pscustomobject]@{Kind='Progress';Stage='Reading OU links and DACL';Target=$ou.Name}}
  $oid=([guid]$ou.ObjectGUID).ToString();$odn=[string]$ou.DistinguishedName
  $gpOptions=V $ou 'gPOptions';$inherit=[pscustomobject]@{Name=$ou.Name;'OU ID'=$oid;DN=$odn;'Block GPO inheritance'=if($null -eq $gpOptions){$null}else{([int]$gpOptions -band 1) -ne 0};'GPO options raw'=$gpOptions;'DACL inheritance protected'=$null}
  foreach($operation in @('GPO links','DACL')){
   $watch=[diagnostics.stopwatch]::StartNew();$status='Succeeded';$message='Collected';$errorId='';$recordCount=0
   try{
    if($operation -eq 'GPO links'){
     $raw=[string](V $ou 'gPLink');$tokens=[regex]::Matches($raw,'\[LDAP://(?<dn>[^\]]+);(?<options>\d+)\]','IgnoreCase')
     if(($tokens|ForEach-Object{$_.Value}) -join '' -ine $raw){throw 'Malformed gpLink; no complete link list was inferred.'}
     $localRows=New-Object collections.ArrayList
     for($i=0;$i -lt $tokens.Count;$i++){
      $gpc=$tokens[$i].Groups['dn'].Value;$options=[int]$tokens[$i].Groups['options'].Value;$match=[regex]::Match($gpc,'(?i)^CN=\{(?<id>[0-9a-f-]{36})\},CN=Policies,CN=System,.+$')
      if(-not $match.Success){throw 'Unrecognized GPO DN reference.'}
      [void]$localRows.Add([pscustomobject]@{Name=$ou.Name;'OU ID'=$oid;'OU DN'=$odn;'GPO ID'=([guid]$match.Groups['id'].Value).ToString();'GPO DN'=$gpc;'Link order'=$tokens.Count-$i;Enabled=($options -band 1) -eq 0;Enforced=($options -band 2) -ne 0;'Options raw'=$options})
     }
     foreach($record in $localRows){[void]$linkRows.Add($record)};$recordCount=$localRows.Count
    }else{
     $acl=Get-PSIOuAcl $server $odn;$localRows=New-Object collections.ArrayList
     foreach($rule in @($acl.Rules)){
      [void]$localRows.Add([pscustomobject]@{Name=$ou.Name;'OU ID'=$oid;'OU DN'=$odn;'Trustee SID'=[string]$rule.IdentityReference;'Access type'=[string]$rule.AccessControlType;'AD rights'=[string]$rule.ActiveDirectoryRights;'Object type GUID'=[string]$rule.ObjectType;'Inherited object type GUID'=[string]$rule.InheritedObjectType;'Inheritance type'=[string]$rule.InheritanceType;Inherited=[bool]$rule.IsInherited;'Inheritance flags'=[string]$rule.InheritanceFlags;'Propagation flags'=[string]$rule.PropagationFlags})
     }
     foreach($record in $localRows){[void]$aceRows.Add($record)};$recordCount=$localRows.Count;$inherit.'DACL inheritance protected'=$acl.Protected
    }
   }catch{$status='Failed';$message=$_.Exception.Message;$errorId=$_.FullyQualifiedErrorId;if($operation -eq 'GPO links'){$linkFailures++}else{$aclFailures++}}
   $watch.Stop();[void]$ledger.Add([pscustomobject]@{Name=$ou.Name;'OU ID'=$oid;Target=$odn;Operation=$operation;Status=$status;'Record count'=$recordCount;'Duration ms'=$watch.ElapsedMilliseconds;'Error ID'=$errorId;Message=$message;'Collected at'=[datetimeoffset]::Now.ToString('o')})
  }
  [void]$inheritRows.Add($inherit)
 }
 Emit 'links' $(if($linkFailures){'Partial'}else{'Succeeded'}) $linkRows.ToArray() 'Per-OU results are recorded in OU collection.'
 Emit 'inheritance' $(if($aclFailures){'Partial'}else{'Succeeded'}) $inheritRows.ToArray() 'GPO block inheritance and DACL protection are separate settings; unavailable DACL flags remain null.'
 Emit 'delegations' $(if($aclFailures){'Partial'}else{'Succeeded'}) $aceRows.ToArray() 'Reported DACL ACEs; not an effective-access calculation.'
 Emit 'objectQueries' 'Succeeded' $ledger.ToArray() 'Outcome ledger captured; inspect each row.'
}

function Invoke-PSIOuCollection([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 6}
   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 6}
    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-PSIOuSchema).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='OrganizationalUnits.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}}
}