Private/Groups/Helpers.ps1
|
function Get-PSIGroupSchema { [ordered]@{ groups=@('Groups',@('Name','Group ID','SAM account name','SID','Scope','Category','Description','Managed by','Location DN','DN','Created','Changed','Direct linked members','Membership status')) members=@('Direct linked members',@('Name','Group ID','Group','Member DN','Member name','Member ID','Member SID','Member type','Resolution status')) nested=@('Nested group links',@('Name','Parent group ID','Parent group','Child group ID','Child group','Child DN')) parents=@('Parent group links',@('Name','Group ID','Group','Parent group ID','Parent group','Parent DN')) objectQueries=@('Member resolution',@('Name','Target','Status','Message')) collection=@('Collection coverage',@('Name','Target','Status','Record count','Collected at','Duration ms','Error ID','Message')) } } function Get-PSIGroupQuerySchema {[ordered]@{groups=@('Groups');collection=@('Collection coverage')}} function Get-PSIGroupValue($o,[string]$p){if($null -ne $o -and $null -ne $o.PSObject.Properties[$p]){return $o.$p};return $null} function Get-PSIGroupParentDN([string]$DN){$escaped=$false;for($i=0;$i -lt $DN.Length;$i++){if($escaped){$escaped=$false;continue};if($DN[$i] -eq '\'){$escaped=$true;continue};if($DN[$i] -eq ','){return $DN.Substring($i+1)}};return $null} function Get-PSIGroupDomainDN([string]$DN){$part=$DN;while($part -and $part -notmatch '^(?i)DC='){$part=Get-PSIGroupParentDN $part};return $part} function Read-PSIGroupRange([string]$Server,[string]$DN,[int]$Start){ $entry=$null;$search=$null try{ $entry=New-Object DirectoryServices.DirectoryEntry(('LDAP://'+$Server+'/'+$DN.Replace('/','\/'))) $search=New-Object DirectoryServices.DirectorySearcher($entry) $search.ReferralChasing=[DirectoryServices.ReferralChasingOption]::None;$search.SearchScope=[DirectoryServices.SearchScope]::Base;$search.Filter='(objectClass=group)' $null=$search.PropertiesToLoad.Add(('member;range='+$Start+'-*')) $result=$search.FindOne();if($null -eq $result){throw 'Group base search returned no object.'} $ranged=@($result.Properties.PropertyNames|Where-Object{[string]$_ -like 'member;range=*'}) $attribute=@();if($ranged.Count){$attribute=@($ranged)}else{$attribute=@($result.Properties.PropertyNames|Where-Object{[string]$_ -eq 'member'})} if($attribute.Count -gt 1){throw 'Multiple member ranges returned.'} if(-not $attribute.Count){return [pscustomobject]@{Attribute=$null;Values=@()}} [pscustomobject]@{Attribute=[string]$attribute[0];Values=@($result.Properties[$attribute[0]]|ForEach-Object{[string]$_})} }finally{if($null -ne $search){$search.Dispose()};if($null -ne $entry){$entry.Dispose()}} } function Get-PSIGroupMemberDNs([string]$Server,[string]$DN){ $start=0;$seen=@{};$values=New-Object collections.ArrayList do{ $range=Read-PSIGroupRange $Server $DN $start if(-not $range.Attribute){if($start -ne 0){throw 'Missing continuation range.'};break} foreach($value in @($range.Values)){if([string]::IsNullOrEmpty($value) -or $seen.ContainsKey([string]$value)){throw 'Empty/duplicate member reference.'};$seen[[string]$value]=$true;[void]$values.Add([string]$value)} if($range.Attribute -ieq 'member'){if($start -ne 0){throw 'Unexpected un-ranged continuation.'};break} $m=[regex]::Match($range.Attribute,'^member;range=(\d+)-(\d+|\*)$','IgnoreCase') if(-not $m.Success -or [int]$m.Groups[1].Value -ne $start){throw 'Unexpected member range.'} if($m.Groups[2].Value -eq '*'){break} $next=[int]$m.Groups[2].Value+1 if($next -le $start -or @($range.Values).Count -ne ($next-$start)){throw 'Non-progressing/incomplete member range.'};$start=$next }while($true) $values.ToArray() } function ConvertTo-PSIGroupModel($Context,[object[]]$Envelopes,[string]$MissingStatus=''){ $schema=Get-PSIGroupSchema;$data=[ordered]@{};$coverage=[ordered]@{};$byKey=@{} foreach($k in $schema.Keys){$data[$k]=New-Object collections.ArrayList;$coverage[$k]=$true} foreach($e in $Envelopes){if($byKey.ContainsKey([string]$e.Key) -or $e.Status -notin @('Succeeded','Failed','TimedOut')){throw 'Duplicate/invalid group outcome.'};$byKey[[string]$e.Key]=$e} if(-not $byKey.ContainsKey('groups')){throw 'Missing group enumeration outcome.'};$enumeration=$byKey.groups;$groupByDN=@{};$groupIds=@{} function Add-Ledger($e,[string]$Name,[string]$Target,[int]$Count){[void]$data.collection.Add([pscustomobject]@{Name=$Name;Target=$Target;Status=$e.Status;'Record count'=$Count;'Collected at'=$e.CollectedAt;'Duration ms'=$e.DurationMs;'Error ID'=$e.ErrorId;Message=$e.ErrorMessage})} if($enumeration.Status -eq 'Succeeded'){ foreach($g in $enumeration.Records){$id=([guid]$g.Id).ToString();if($groupIds.ContainsKey($id) -or $groupByDN.ContainsKey([string]$g.DN) -or (Get-PSIGroupDomainDN $g.DN) -ine $Context.DomainDN){throw 'Duplicate/out-of-domain group.'};$groupIds[$id]=$true;$groupByDN[[string]$g.DN]=$g [void]$data.groups.Add([pscustomobject]@{Name=$g.Name;'Group ID'=$id;'SAM account name'=$g.Sam;SID=$g.Sid;Scope=$g.Scope;Category=$g.Category;Description=$g.Description;'Managed by'=$g.ManagedBy;'Location DN'=Get-PSIGroupParentDN $g.DN;DN=$g.DN;Created=$g.Created;Changed=$g.Changed;'Direct linked members'=$null;'Membership status'='Unknown'}) } }else{foreach($k in @('groups','members','nested','parents','objectQueries')){$coverage[$k]=$false}} Add-Ledger $enumeration 'Group enumeration' $Context.Domain $data.groups.Count foreach($g in $data.groups){$key='members:'+$g.'Group ID' if(-not $byKey.ContainsKey($key)){if($MissingStatus -notin @('Failed','TimedOut')){throw "Missing membership outcome: $key"};$byKey[$key]=[pscustomobject]@{Status=$MissingStatus;Records=@();Lookups=@();CollectedAt=[datetimeoffset]::Now.ToString('o');DurationMs=0;ErrorId='GroupInventory.Incomplete';ErrorMessage='Membership worker did not complete this group.'}} $e=$byKey[$key];$g.'Membership status'=$e.Status if($e.Status -eq 'Succeeded'){ $g.'Direct linked members'=@($e.Records).Count;$seenMembers=@{} foreach($member in $e.Records){if($seenMembers.ContainsKey([string]$member.DN)){throw 'Duplicate member reference.'};$seenMembers[[string]$member.DN]=$true [void]$data.members.Add([pscustomobject]@{Name=$g.Name+' / '+$member.DN;'Group ID'=$g.'Group ID';Group=$g.Name;'Member DN'=$member.DN;'Member name'=$member.Name;'Member ID'=$member.Id;'Member SID'=$member.Sid;'Member type'=$member.Type;'Resolution status'=$member.Resolution}) if($groupByDN.ContainsKey([string]$member.DN)){$child=$groupByDN[[string]$member.DN];[void]$data.nested.Add([pscustomobject]@{Name=$g.Name+' → '+$child.Name;'Parent group ID'=$g.'Group ID';'Parent group'=$g.Name;'Child group ID'=$child.Id;'Child group'=$child.Name;'Child DN'=$child.DN});[void]$data.parents.Add([pscustomobject]@{Name=$child.Name+' ← '+$g.Name;'Group ID'=$child.Id;Group=$child.Name;'Parent group ID'=$g.'Group ID';'Parent group'=$g.Name;'Parent DN'=$g.DN})} } foreach($lookup in @($e.Lookups)){[void]$data.objectQueries.Add([pscustomobject]@{Name=$g.Name;Target=$lookup.Target;Status=$lookup.Status;Message=$lookup.Message});if($lookup.Status -eq 'Failed'){$coverage.objectQueries=$false}} }else{foreach($k in @('members','nested','parents','objectQueries')){$coverage[$k]=$false}} Add-Ledger $e ('Direct members: '+$g.Name) $g.DN $(if($e.Status -eq 'Succeeded'){@($e.Records).Count}else{0}) } foreach($key in $byKey.Keys){if($key -ne 'groups' -and ($key -notlike 'members:*' -or -not $groupIds.ContainsKey($key.Substring(8)))){throw "Unexpected group outcome: $key"}} $arrays=[ordered]@{};foreach($k in $schema.Keys){$arrays[$k]=@($data[$k].ToArray())} [pscustomobject]@{Schema=$schema;Data=$arrays;Coverage=$coverage;Raw=[ordered]@{}} } |