internal/functions/Resolve-ContentSearchBase.ps1
function Resolve-ContentSearchBase { <# .SYNOPSIS Resolves the ruleset for content enforcement into actionable search data. .DESCRIPTION Resolves the ruleset for content enforcement into actionable search data. This ensures that both Include and Exclude rules are properly translated into AD search queries. This command is designed to be called by all Test- commands across the entire module. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER NoContainer By defaults, containers are returned as well. Using this parameter prevents container processing. .PARAMETER IgnoreMissingSearchbase Disables warnings if a defined searchbase is missing. For use in OU tests. .EXAMPLE PS C:\> Resolve-ContentSearchBase @parameters Resolves the configured filters into searchbases for the targeted domain. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] Param ( [string] $Server, [pscredential] $Credential, [switch] $NoContainer, [switch] $IgnoreMissingSearchbase ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false #region Utility Functions function Convert-DistinguishedName { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [string[]] $Name, [switch] $Exclude ) process { foreach ($nameItem in $Name) { [PSCustomObject]@{ Name = $nameItem Depth = ($nameItem -split "," | Where-Object { $_ -notlike "DC=*" }).Count Elements = ($nameItem -split "," | Where-Object { $_ -notlike "DC=*" }) Exclude = $Exclude.ToBool() } } } } function Get-ChildRelationship { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $Parent, [Parameter(Mandatory = $true)] $Items ) foreach ($item in $Items) { if ($item.Name -notlike "*,$($Parent.Name)") { continue } [PSCustomObject]@{ Child = $item Parent = $Parent Delta = $item.Depth - $Parent.Depth } } } function New-SearchBase { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Name, [ValidateSet('OneLevel', 'Subtree')] [string] $Scope = 'Subtree' ) [PSCustomObject]@{ SearchBase = $Name SearchScope = $Scope } } function Resolve-SearchBase { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] $Parent, [Parameter(Mandatory = $true)] $Children, [string] $Server, [pscredential] $Credential ) New-SearchBase -Name $Parent.Name -Scope OneLevel $childPaths = @{ $Parent.Name = @{} } foreach ($childItem in $Children) { $subPath = $childItem.Name.Replace($Parent.Name, '').Trim(",") $subPathSegments = $subPath.Split(",") [System.Array]::Reverse($subPathSegments) $basePath = $Parent.Name foreach ($pathSegment in $subPathSegments) { $newDN = $pathSegment, $basePath -join "," $childPaths[$basePath][$newDN] = $newDN if (-not $childPaths[$newDN]) { $childPaths[$newDN] = @{ } } $basePath = $newDN } } $currentPath = '' [System.Collections.ArrayList]$pathsToProcess = @($Parent.Name) while ($pathsToProcess.Count -gt 0) { $currentPath = $pathsToProcess[0] $nextContainerObjects = Get-ADObject @parameters -SearchBase $currentPath -SearchScope OneLevel -LDAPFilter '(|(objectCategory=container)(objectCategory=organizationalUnit))' foreach ($containerObject in $nextContainerObjects) { # Skip the actual children, as those (and their children) have already been processed if ($containerObject.DistinguishedName -in $Children.Name) { continue } if ($childPaths.ContainsKey($containerObject.DistinguishedName)) { New-SearchBase -Name $containerObject.DistinguishedName -Scope OneLevel $null = $pathsToProcess.Add($containerObject.DistinguishedName) } else { New-SearchBase -Name $containerObject.DistinguishedName } } $pathsToProcess.Remove($currentPath) } } #endregion Utility Functions Set-DMDomainContext @parameters $warningLevel = 'Warning' $domain = Get-Domain2 @parameters if (@(Get-ADOrganizationalUnit @parameters -ErrorAction Ignore -ResultSetSize 2 -Filter * -SearchBase $domain).Count -eq 1) { $warningLevel = 'Verbose' } } process { #region preprocessing and early termination # Don't process any OUs if in Additive Mode if ($script:contentMode.Mode -eq 'Additive') { return } # If already processed, return previous results if (($Server -eq $script:contentSearchBases.Server) -and (-not (Compare-Object $script:contentMode.Include $script:contentSearchBases.Include)) -and (-not (Compare-Object $script:contentMode.Exclude $script:contentSearchBases.Exclude))) { if ($NoContainer) { $script:contentSearchBases.Bases | Where-Object SearchBase -notlike "CN=*" } else { $script:contentSearchBases.Bases } return } # Parse Includes and excludes $include = $script:contentMode.Include | Resolve-String | Convert-DistinguishedName $exclude = $script:contentMode.Exclude | Resolve-String | Convert-DistinguishedName -Exclude # If no todo: Terminate if (-not ($include -or $exclude)) { return } # Implicitly include domain when no custom include rules if ($exclude -and -not $include) { $include = $script:domainContext.DN | Convert-DistinguishedName } $allItems = @{} foreach ($item in $include) { if (-not (Test-ADObject @parameters -Identity $item.Name)) { if ($IgnoreMissingSearchbase) { continue } Write-PSFMessage -Level $warningLevel -String 'Resolve-ContentSearchBase.Include.NotFound' -StringValues $item.Name -Tag notfound, container -Target $Server continue } $allItems[$item.Name] = $item } foreach ($item in $exclude) { if (-not (Test-ADObject @parameters -Identity $item.Name)) { if ($IgnoreMissingSearchbase) { continue } Write-PSFMessage -Level $warningLevel -String 'Resolve-ContentSearchBase.Exclude.NotFound' -StringValues $item.Name -Tag notfound, container -Target $Server continue } $allItems[$item.Name] = $item } $relationship_All = foreach ($item in $allItems.Values) { Get-ChildRelationship -Parent $item -Items $allItems.Values } # Remove multiple include/exclude nestings producing reddundant inheritance detection $relationship_Relevant = $relationship_All | Group-Object { $_.Child.Name } | ForEach-Object { $_.Group | Sort-Object Delta | Select-Object -First 1 } #endregion preprocessing and early termination [System.Collections.ArrayList]$itemsProcessed = @() [System.Collections.ArrayList]$targetOUsFound = @() foreach ($item in ($allItems.Values | Sort-Object Depth -Descending)) { $children = $relationship_Relevant | Where-Object { $_.Parent.Name -eq $item.Name } $allChildren = $relationship_All | Where-Object { $_.Parent.Name -eq $item.Name } # Case: Exclude Rule - will not be scanned if ($item.Exclude) { $null = $itemsProcessed.Add($item) continue } # Casse: No Children - Just add a plain searchbase if (-not $children) { $null = $targetOUsFound.Add((New-SearchBase -Name $item.Name)) $null = $itemsProcessed.Add($item) continue } # Case: No recursive Children that would exclude something - Add plain searchbase and remove all entries from all children as not needed if (-not ($allChildren.Child | Where-Object Exclude)) { $redundantFindings = $targetOUsFound | Where-Object SearchBase -in $allChildren.Child.Name foreach ($finding in $redundantFindings) { $targetOUsFound.Remove($finding) } $null = $targetOUsFound.Add((New-SearchBase -Name $item.Name)) $null = $itemsProcessed.Add($item) continue } # Case: Children that require processing foreach ($searchbase in (Resolve-SearchBase @parameters -Parent $item -Children $children.Child)) { $null = $targetOUsFound.Add($searchbase) } $null = $itemsProcessed.Add($item) } $script:contentSearchBases.Include = $script:contentMode.Include $script:contentSearchBases.Exclude = $script:contentMode.Exclude $script:contentSearchBases.Server = $Server $script:contentSearchBases.Bases = $targetOUsFound.ToArray() foreach ($searchBase in $script:contentSearchBases.Bases) { if ($NoContainer -and ($searchBase.SearchBase -like 'CN=*')) { continue } Write-PSFMessage -String 'Resolve-ContentSearchBase.Searchbase.Found' -StringValues $searchBase.SearchScope, $searchBase.SearchBase, $script:domainContext.Fqdn $searchBase } } } |