Src/Private/Get-AbrSPSharingPolicy.ps1

function Get-AbrSPSharingPolicy {
    <#
    .SYNOPSIS
    Documents SharePoint Online and OneDrive external sharing policies and link settings.
    .DESCRIPTION
        Collects and reports on:
          - Tenant-level external sharing levels (SharePoint and OneDrive)
          - Anonymous 'Anyone' link settings and expiry
          - Default sharing link type and permissions
          - Domain allow/block lists
          - Guest access expiry settings
          - ACSC E8 and CIS compliance assessment (if enabled)
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting SharePoint Sharing Policy data for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Sharing Policy'
    }

    process {
        Section -Style Heading2 'Sharing Policy' {
            Paragraph "The following section documents the external sharing configuration for SharePoint Online and OneDrive for Business in tenant $TenantId. Sharing settings control who can access content outside the organisation."
            BlankLine

            # Initialise variables used by compliance checks
            $SPExternalSharingLevel    = 'Unknown'
            $ODExternalSharingLevel    = 'Unknown'
            $SPExternalSharingLevelNum = 99
            $ODExternalSharingLevelNum = 99
            $AnyoneLinkEnabled         = $false
            $AnyoneLinkExpiryDays      = 0
            $DefaultSharingLinkType    = 'Unknown'
            $RequireAcceptingAccountMatchInvitedAccount = $false
            $NotifyOwnersWhenItemsShared = $false
            $ExternalUserExpireInDays  = 0

            # Map SPO sharing level enum values to readable strings and numeric rank
            $SharingLevelMap = @{
                'Disabled'                      = @{ Label = 'Disabled'; Rank = 0 }
                'ExistingExternalUserSharingOnly'= @{ Label = 'Existing guests only'; Rank = 1 }
                'ExternalUserSharingOnly'        = @{ Label = 'New and existing guests'; Rank = 2 }
                'ExternalUserAndGuestSharing'    = @{ Label = 'Anyone (including anonymous)'; Rank = 3 }
            }

            #region Sharing Settings via PnP
            try {
                if (-not $script:PnPAvailable) {
                    Paragraph " [!] PnP.PowerShell is not available. Detailed sharing policy settings require PnP.PowerShell. Install with: Install-Module -Name PnP.PowerShell -Force"
                } else {
                    $SPTenant = Get-PnPTenant -ErrorAction Stop

                    # Decode sharing levels
                    $spLevelKey = $(ConvertTo-SPEnumString $SPTenant.SharingCapability 'Disabled')
                    $odLevelKey = $(ConvertTo-SPEnumString $SPTenant.OneDriveSharingCapability 'Disabled')

                    $SPExternalSharingLevel    = if ($SharingLevelMap[$spLevelKey]) { $SharingLevelMap[$spLevelKey].Label } else { $spLevelKey }
                    $ODExternalSharingLevel    = if ($SharingLevelMap[$odLevelKey]) { $SharingLevelMap[$odLevelKey].Label } else { $odLevelKey }
                    $SPExternalSharingLevelNum = if ($SharingLevelMap[$spLevelKey]) { $SharingLevelMap[$spLevelKey].Rank } else { 99 }
                    $ODExternalSharingLevelNum = if ($SharingLevelMap[$odLevelKey]) { $SharingLevelMap[$odLevelKey].Rank } else { 99 }

                    $AnyoneLinkEnabled         = ($spLevelKey -eq 'ExternalUserAndGuestSharing')
                    $AnyoneLinkExpiryDays      = $SPTenant.RequireAnonymousLinksExpireInDays
                    $AnyoneLinkState           = if ($AnyoneLinkEnabled) { 'Enabled' } else { 'Disabled' }

                    $DefaultSharingLinkType    = $(ConvertTo-SPEnumString $SPTenant.DefaultSharingLinkType 'Direct')
                    $RequireAcceptingAccountMatchInvitedAccount = $SPTenant.RequireAcceptingAccountMatchInvitedAccount
                    $NotifyOwnersWhenItemsShared = $SPTenant.NotifyOwnersWhenItemsReshared
                    $ExternalUserExpireInDays  = $SPTenant.ExternalUserExpireInDays

                    #region Sharing Summary Table
                    $ShareObj = [System.Collections.ArrayList]::new()
                    $shareInObj = [ordered] @{
                        'SharePoint External Sharing'             = $SPExternalSharingLevel
                        'OneDrive External Sharing'               = $ODExternalSharingLevel
                        'Anonymous (Anyone) Links'                = $AnyoneLinkState
                        'Anyone Link Expiry (days)'               = if ($AnyoneLinkExpiryDays -gt 0) { $AnyoneLinkExpiryDays } else { 'No expiry set' }
                        'Default Sharing Link Type'               = $DefaultSharingLinkType
                        'Default Link Permission'                 = $(ConvertTo-SPEnumString $SPTenant.DefaultLinkPermission 'View')
                        'Require Account Match for Invitations'   = $RequireAcceptingAccountMatchInvitedAccount
                        'Notify Owners When Items Re-shared'      = $NotifyOwnersWhenItemsShared
                        'Guest Access Expiry (days)'              = if ($ExternalUserExpireInDays -gt 0) { $ExternalUserExpireInDays } else { 'Not configured' }
                        'Prevent External Users from Re-sharing'  = $SPTenant.PreventExternalUsersFromResharing
                        'Allow Guests Sign In with Email'         = $SPTenant.AllowGuestUserToSignIn
                    }
                    $ShareObj.Add([pscustomobject](ConvertTo-HashToYN $shareInObj)) | Out-Null

                    $null = (& {
                        if ($HealthCheck.SharePoint.SharingPolicy) {
                            # Flag 'Anyone' sharing as critical
                            $null = ($ShareObj | Where-Object { $_.'SharePoint External Sharing' -like '*Anyone*' } | Set-Style -Style Critical | Out-Null)
                            $null = ($ShareObj | Where-Object { $_.'OneDrive External Sharing'   -like '*Anyone*' } | Set-Style -Style Critical | Out-Null)
                        }
                    })

                    $TableParams = @{ Name = "External Sharing Settings - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                    if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" }
                    $ShareObj | Table @TableParams
                    BlankLine
                    #endregion

                    #region Domain Allow/Block List
                    try {
                        $AllowedDomains = $SPTenant.SharingAllowedDomainList
                        $BlockedDomains = $SPTenant.SharingBlockedDomainList
                        $DomainMode     = $(ConvertTo-SPEnumString $SPTenant.SharingDomainRestrictionMode 'None')

                        if ($DomainMode -ne 'None' -or $AllowedDomains -or $BlockedDomains) {
                            $DomainObj = [System.Collections.ArrayList]::new()
                            $domainInObj = [ordered] @{
                                'Domain Restriction Mode' = $DomainMode
                                'Allowed Domains'         = if ($AllowedDomains) { $AllowedDomains -join ', ' } else { 'None configured' }
                                'Blocked Domains'         = if ($BlockedDomains) { $BlockedDomains -join ', ' } else { 'None configured' }
                            }
                            $DomainObj.Add([pscustomobject]$domainInObj) | Out-Null
                            $DomainTableParams = @{ Name = "Sharing Domain Restrictions - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                            if ($Report.ShowTableCaptions) { $DomainTableParams['Caption'] = "- $($DomainTableParams.Name)" }
                            $DomainObj | Table @DomainTableParams
                            BlankLine
                        }
                    } catch {
                        Write-AbrDebugLog "Could not retrieve domain restriction settings: $($_.Exception.Message)" 'WARN' 'SHARING'
                    }
                    #endregion

                    # Add to Excel
                    $script:ExcelSheets['Sharing Policy'] = $ShareObj
                }
            } catch {
                Write-AbrSectionError -Section 'Sharing Policy' -Message "$($_.Exception.Message)"
            }
            #endregion

            #region ACSC E8 Assessment
            if ($script:IncludeACSCe8) {
                BlankLine
                Paragraph "ACSC Essential Eight Assessment -- External Sharing"
                $E8Defs   = Get-AbrSPE8Checks -Section 'SharingPolicy'
                $E8Vars   = @{
                    SPExternalSharingLevel    = $SPExternalSharingLevel
                    ODExternalSharingLevel    = $ODExternalSharingLevel
                    SPExternalSharingLevelNum = $SPExternalSharingLevelNum
                    ODExternalSharingLevelNum = $ODExternalSharingLevelNum
                    AnyoneLinkEnabled         = $AnyoneLinkEnabled
                    AnyoneLinkExpiryDays      = $AnyoneLinkExpiryDays
                    AnyoneLinkState           = if ($AnyoneLinkEnabled) { 'Enabled' } else { 'Disabled' }
                    RequireAcceptingAccountMatchInvitedAccount = $RequireAcceptingAccountMatchInvitedAccount
                    NotifyOwnersWhenItemsShared = $NotifyOwnersWhenItemsShared
                }
                $E8Checks = Build-AbrSPComplianceChecks -Definitions $E8Defs -Framework 'E8' -CallerVariables $E8Vars
                New-AbrSPE8AssessmentTable -Checks $E8Checks -Name 'Sharing Policy' -TenantId $TenantId

                # Accumulate for consolidated E8 sheet
                foreach ($row in $E8Checks) {
                    $null = $script:E8AllChecks.Add([pscustomobject](@{ Section = 'Sharing Policy' } + ($row | ConvertTo-HashTableSP)))
                }
            }
            #endregion

            #region CIS Baseline Assessment
            if ($script:IncludeCISBaseline) {
                BlankLine
                Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Sharing Policy"
                $CISDefs   = Get-AbrSPCISChecks -Section 'SharingPolicy'
                $CISVars   = @{
                    SPExternalSharingLevel    = $SPExternalSharingLevel
                    ODExternalSharingLevel    = $ODExternalSharingLevel
                    AnyoneLinkEnabled         = $AnyoneLinkEnabled
                    AnyoneLinkExpiryDays      = $AnyoneLinkExpiryDays
                    DefaultSharingLinkType    = $DefaultSharingLinkType
                    RequireAcceptingAccountMatchInvitedAccount = $RequireAcceptingAccountMatchInvitedAccount
                    ExternalUserExpireInDays  = $ExternalUserExpireInDays
                }
                $CISChecks = Build-AbrSPComplianceChecks -Definitions $CISDefs -Framework 'CIS' -CallerVariables $CISVars
                New-AbrSPCISAssessmentTable -Checks $CISChecks -Name 'Sharing Policy' -TenantId $TenantId

                foreach ($row in $CISChecks) {
                    $null = $script:CISAllChecks.Add([pscustomobject](@{ Section = 'Sharing Policy' } + ($row | ConvertTo-HashTableSP)))
                }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Sharing Policy'
    }
}

function ConvertTo-HashTableSP {
    <#
    .SYNOPSIS
    Converts a PSCustomObject to an ordered hashtable. Used for Excel accumulation.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory, ValueFromPipeline)][pscustomobject]$InputObject)
    process {
        $ht = [ordered]@{}
        foreach ($prop in $InputObject.PSObject.Properties) {
            $ht[$prop.Name] = $prop.Value
        }
        return $ht
    }
}