Private/LocationScope.ps1

<#
.SYNOPSIS
    Location scope functions for DLP policy location handling.

.DESCRIPTION
    This file contains functions for determining and managing location scopes
    in DLP policies, including detecting "All" vs specific targeting, extracting
    location details, and converting locations during cross-tenant migrations.

.NOTES
    Internal module file - not exported to users.
    Functions are available to all module cmdlets.
    
    IMPORTANT: Microsoft stores location data differently than expected:
    - Location arrays like OneDriveLocation contain [{"Name":"All","Type":"Tenant"}] even for specific targeting
    - Actual targeting is determined by scoping properties like OneDriveSharedBy, ExchangeSender, etc.
    - This module uses the comprehensive logic to correctly identify "All" locations
#>


function Test-IsAllLocation {
    <#
    .SYNOPSIS
        Determines if a location is scoped to "All" (tenant-wide) vs specific users/groups/sites.
    
    .DESCRIPTION
        Microsoft stores location data differently than expected:
        - Location arrays like OneDriveLocation contain [{"Name":"All","Type":"Tenant"}] even for specific targeting
        - Actual targeting is determined by scoping properties like OneDriveSharedBy, ExchangeSender, etc.
        
        This function checks both the location array AND relevant scoping properties to accurately
        determine if a location is truly set to "All" or is targeting specific entities.
    
    .PARAMETER LocationArray
        The location array from Get-DlpCompliancePolicy (e.g., OneDriveLocation, ExchangeLocation).
    
    .PARAMETER ScopingProperties
        Additional properties that indicate specific targeting (e.g., OneDriveSharedBy, ExchangeSender).
        If any scoping properties are populated, the location is NOT "All".
    
    .PARAMETER AdaptiveScopeProperties
        Adaptive scope properties (e.g., ExchangeAdaptiveScopes, SharePointAdaptiveScopes).
        If any adaptive scopes are configured, the location is NOT "All".
    
    .OUTPUTS
        System.Boolean
        Returns $true if location is "All" (tenant-wide), $false if specific or not configured.
    
    .EXAMPLE
        $isAll = Test-IsAllLocation -LocationArray $policy.ExchangeLocation `
            -ScopingProperties @($policy.ExchangeSender, $policy.ExchangeSenderMemberOf) `
            -AdaptiveScopeProperties @($policy.ExchangeAdaptiveScopes)
    
    .NOTES
        This is the comprehensive version from Get-DLPPolicyConfiguration.ps1.
        It correctly handles all edge cases for location scope detection.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $false)]
        [object]$LocationArray,
        
        [Parameter(Mandatory = $false)]
        [object[]]$ScopingProperties = @(),
        
        [Parameter(Mandatory = $false)]
        [object[]]$AdaptiveScopeProperties = @()
    )
    
    # Not configured if array is empty or null
    if (-not $LocationArray -or $LocationArray.Count -eq 0) {
        return $false
    }
    
    # Check if adaptive scopes are configured (always specific targeting)
    foreach ($adaptiveScope in $AdaptiveScopeProperties) {
        if ($adaptiveScope -and @($adaptiveScope).Count -gt 0) {
            Write-Verbose "Location has adaptive scopes - not 'All'"
            return $false  # Has adaptive scopes = specific targeting
        }
    }
    
    # Check if scoping properties indicate specific targeting
    foreach ($scopingProp in $ScopingProperties) {
        if ($scopingProp -and @($scopingProp).Count -gt 0) {
            Write-Verbose "Location has scoping properties - not 'All'"
            return $false  # Has specific scoping = not "All"
        }
    }
    
    # Check the location array structure
    $locationList = @($LocationArray)
    
    # If exactly one location and it's Type="Tenant" with Name="All", it's truly "All"
    if ($locationList.Count -eq 1) {
        $firstLocation = $locationList[0]
        
        # Handle object with Type property
        if ($firstLocation.PSObject.Properties['Type'] -and 
            $firstLocation.Type.Value -eq 'Tenant' -and 
            $firstLocation.Name -eq 'All') {
            Write-Verbose "Location is 'All' (Type=Tenant)"
            return $true
        }
        
        # Handle simple string "All"
        if ($firstLocation -eq 'All') {
            Write-Verbose "Location is 'All' (string)"
            return $true
        }
    }
    
    Write-Verbose "Location has multiple entries or specific sites/groups - not 'All'"
    return $false  # Multiple locations or specific sites/groups
}

function Get-LocationScopeInfo {
    <#
    .SYNOPSIS
        Gets detailed scope information for a workload location.
    
    .DESCRIPTION
        Returns comprehensive information about a location's configuration including:
        - Whether the location is configured at all
        - Whether it's set to "All" (tenant-wide)
        - Whether it's specific targeting
        - Count of targeted entities
        - Formatted string of location identifiers
    
    .PARAMETER LocationArray
        The location array from Get-DlpCompliancePolicy (e.g., OneDriveLocation, ExchangeLocation).
    
    .PARAMETER ScopingProperties
        Additional properties that indicate specific targeting (e.g., OneDriveSharedBy, ExchangeSender).
    
    .PARAMETER AdaptiveScopeProperties
        Adaptive scope properties (e.g., ExchangeAdaptiveScopes, SharePointAdaptiveScopes).
    
    .PARAMETER LocationName
        The friendly name of the location (e.g., "Exchange", "OneDrive") for logging purposes.
    
    .OUTPUTS
        System.Collections.Hashtable
        Returns a hashtable with keys:
        - IsConfigured (bool): Location is configured
        - IsAll (bool): Location is set to "All"
        - IsSpecific (bool): Location has specific targeting
        - Count (int): Number of targeted entities
        - Locations (string): Formatted string of identifiers
    
    .EXAMPLE
        $exchangeScope = Get-LocationScopeInfo -LocationArray $policy.ExchangeLocation `
            -LocationName "Exchange" `
            -ScopingProperties @($policy.ExchangeSender, $policy.ExchangeSenderMemberOf) `
            -AdaptiveScopeProperties @($policy.ExchangeAdaptiveScopes)
        
        if ($exchangeScope.IsAll) {
            Write-Host "Exchange location is set to All"
        }
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $false)]
        [object]$LocationArray,
        
        [Parameter(Mandatory = $false)]
        [object[]]$ScopingProperties = @(),
        
        [Parameter(Mandatory = $false)]
        [object[]]$AdaptiveScopeProperties = @(),
        
        [Parameter(Mandatory = $false)]
        [string]$LocationName = "Location"
    )
    
    # Return not configured if location array is empty
    if (-not $LocationArray -or $LocationArray.Count -eq 0) {
        Write-Verbose "$LocationName is not configured"
        return @{
            IsConfigured = $false
            IsAll = $false
            IsSpecific = $false
            Count = 0
            Locations = ""
        }
    }
    
    # Determine if location is "All" or specific
    $isAll = Test-IsAllLocation -LocationArray $LocationArray `
        -ScopingProperties $ScopingProperties `
        -AdaptiveScopeProperties $AdaptiveScopeProperties
    
    $locationList = @($LocationArray)
    
    # Build location string based on type
    $locationString = ""
    if ($isAll) {
        $locationString = "All"
    }
    else {
        # Extract meaningful identifiers from scoping properties and adaptive scopes
        $identifiers = @()
        
        # Check scoping properties first (most specific)
        foreach ($scopingProp in $ScopingProperties) {
            if ($scopingProp -and @($scopingProp).Count -gt 0) {
                foreach ($item in @($scopingProp)) {
                    if ($item -is [string]) {
                        # Might be JSON string, try to extract email/name
                        if ($item -match '"PrimarySmtpAddress":"([^"]+)"') {
                            $identifiers += $matches[1]
                        }
                        elseif ($item -match '"DisplayName":"([^"]+)"') {
                            $identifiers += $matches[1]
                        }
                        else {
                            $identifiers += $item
                        }
                    }
                    else {
                        # Object with properties
                        if ($item.PrimarySmtpAddress) {
                            $identifiers += $item.PrimarySmtpAddress
                        }
                        elseif ($item.DisplayName) {
                            $identifiers += $item.DisplayName
                        }
                        elseif ($item.Name) {
                            $identifiers += $item.Name
                        }
                    }
                }
            }
        }
        
        # Check adaptive scopes
        foreach ($adaptiveScope in $AdaptiveScopeProperties) {
            if ($adaptiveScope -and @($adaptiveScope).Count -gt 0) {
                foreach ($item in @($adaptiveScope)) {
                    if ($item.DisplayName) {
                        $identifiers += $item.DisplayName
                    }
                    elseif ($item.Name) {
                        $identifiers += $item.Name
                    }
                    else {
                        $identifiers += $item.ToString()
                    }
                }
            }
        }
        
        # Fallback to location array contents
        if ($identifiers.Count -eq 0) {
            foreach ($loc in $locationList) {
                if ($loc.DisplayName -and $loc.DisplayName -ne 'All') {
                    $identifiers += $loc.DisplayName
                }
                elseif ($loc.Name -and $loc.Name -ne 'All') {
                    $identifiers += $loc.Name
                }
                elseif ($loc -is [string] -and $loc -ne 'All') {
                    $identifiers += $loc
                }
            }
        }
        
        $locationString = $identifiers -join "; "
        if ([string]::IsNullOrWhiteSpace($locationString)) {
            $locationString = "Specific (details in raw properties)"
        }
    }
    
    $result = @{
        IsConfigured = $true
        IsAll = $isAll
        IsSpecific = -not $isAll
        Count = if ($isAll) { 1 } else { ([array]$identifiers).Count }
        Locations = $locationString
    }
    
    Write-Verbose "$LocationName scope: IsAll=$($result.IsAll), Count=$($result.Count)"
    return $result
}

function ConvertTo-AllLocation {
    <#
    .SYNOPSIS
        Converts specific location targeting to "All" for cross-tenant migration.
    
    .DESCRIPTION
        When migrating DLP policies across tenants, specific user/group/site targeting
        cannot be preserved because the identities don't exist in the destination tenant.
        This function converts specific targeting to "All" (tenant-wide) and clears
        all scoping properties and exceptions.
    
    .PARAMETER LocationType
        The type of location being converted (e.g., "Exchange", "SharePoint", "OneDrive", "Teams").
    
    .OUTPUTS
        System.Collections.Hashtable
        Returns a hashtable with keys:
        - Location: Set to @("All")
        - ScopingProperties: Empty hashtable for all scoping properties
        - AdaptiveScopeProperties: Empty hashtable for adaptive scope properties
        - ExceptionProperties: Empty hashtable for all exception properties
    
    .EXAMPLE
        $allLocations = ConvertTo-AllLocation -LocationType "Exchange"
        # Returns configuration to set Exchange to "All" with no scoping
    
    .EXAMPLE
        $sharepointAll = ConvertTo-AllLocation -LocationType "SharePoint"
        # Returns configuration to set SharePoint to "All" with no exceptions
    
    .NOTES
        This function is primarily used during cross-tenant migration mode.
        It ensures policies can be recreated in destination tenant without
        referencing non-existent users, groups, or sites.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Exchange', 'SharePoint', 'OneDrive', 'Teams')]
        [string]$LocationType
    )
    
    $result = @{
        Location = @('All')
        ScopingProperties = @{}
        AdaptiveScopeProperties = @{}
        ExceptionProperties = @{}
    }
    
    # Location-specific scoping and exception properties
    switch ($LocationType) {
        'Exchange' {
            $result.ScopingProperties = @{
                ExchangeSender = @()
                ExchangeSenderMemberOf = @()
            }
            $result.AdaptiveScopeProperties = @{
                ExchangeAdaptiveScopes = @()
            }
            $result.ExceptionProperties = @{
                ExchangeLocationException = @()
                ExchangeSenderException = @()
                ExchangeSenderMemberOfException = @()
            }
        }
        'SharePoint' {
            $result.ScopingProperties = @{}  # SharePoint doesn't have sender-type scoping
            $result.AdaptiveScopeProperties = @{
                SharePointAdaptiveScopes = @()
            }
            $result.ExceptionProperties = @{
                SharePointLocationException = @()
            }
        }
        'OneDrive' {
            $result.ScopingProperties = @{
                OneDriveSharedBy = @()
                OneDriveSharedByMemberOf = @()
            }
            $result.AdaptiveScopeProperties = @{
                OneDriveAdaptiveScopes = @()
            }
            $result.ExceptionProperties = @{
                OneDriveLocationException = @()
                OneDriveSharedByException = @()
                OneDriveSharedByMemberOfException = @()
            }
        }
        'Teams' {
            $result.ScopingProperties = @{
                TeamsSender = @()
                TeamsSenderMemberOf = @()
            }
            $result.AdaptiveScopeProperties = @{
                TeamsAdaptiveScopes = @()
            }
            $result.ExceptionProperties = @{
                TeamsLocationException = @()
                TeamsSenderException = @()
                TeamsSenderMemberOfException = @()
            }
        }
    }
    
    Write-Verbose "Converted $LocationType location to 'All' (cleared all scoping and exceptions)"
    return $result
}