Src/Private/Get-AbrExoSafeAttachmentsSafeLinks.ps1

function Get-AbrExoSafeAttachmentsSafeLinks {
    <#
    .SYNOPSIS
    Documents Microsoft Defender for Office 365 Safe Attachments and Safe Links policies
    with ACSC E8 and CIS compliance assessments.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

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

    begin {
        Write-PScriboMessage -Message "Collecting Safe Attachments and Safe Links configuration for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'SafeAttachmentsSafeLinks'
    }

    process {
        Section -Style Heading2 'Safe Attachments & Safe Links' {
            Paragraph "The following section documents Safe Attachments and Safe Links policies in tenant $TenantId. These features require a Microsoft Defender for Office 365 Plan 1 or higher licence."
            BlankLine

            # Compliance variable defaults
            $SafeAttachPolicyCount      = 0
            $SafeAttachBlockActionCount = 0
            $SafeAttachForSPOEnabled    = $false
            $SafeLinksPolicyCount       = 0
            $SafeLinksDoNotRewriteUrlCount = 0
            $SafeLinksRealTimeCount     = 0

            #region Safe Attachments
            try {
                Write-Host " - Retrieving Safe Attachments policies..."
                $SafeAttachPolicies = Get-SafeAttachmentPolicy -ErrorAction Stop | Sort-Object IsDefault -Descending

                $SafeAttachPolicyCount       = @($SafeAttachPolicies | Where-Object { $_.Enable -eq $true }).Count
                $SafeAttachBlockActionCount  = @($SafeAttachPolicies | Where-Object {
                    $_.Enable -eq $true -and $_.Action -in @('Block', 'DynamicDelivery', 'Replace')
                }).Count

                # Check ATP policy for SPO/OneDrive/Teams
                try {
                    $AtpPolicy = Get-AtpPolicyForO365 -ErrorAction SilentlyContinue
                    $SafeAttachForSPOEnabled = ($AtpPolicy.EnableATPForSPOTeamsODB -eq $true)
                } catch { }

                Section -Style Heading3 'Safe Attachments Policies' {
                    if ($SafeAttachPolicies) {
                        Paragraph "$(@($SafeAttachPolicies).Count) Safe Attachments policy/policies found. $SafeAttachPolicyCount are enabled."
                        BlankLine

                        $SAObj = [System.Collections.ArrayList]::new()
                        foreach ($Policy in $SafeAttachPolicies) {
                            $saInObj = [ordered] @{
                                'Policy Name'            = $Policy.Name
                                'Enabled'                = $Policy.Enable
                                'Default Policy'         = $Policy.IsDefault
                                'Action'                 = $Policy.Action
                                'Dynamic Delivery'       = $Policy.Action -eq 'DynamicDelivery'
                                'Quarantine Tag'         = if ($Policy.QuarantineTag) { $Policy.QuarantineTag } else { 'Default' }
                                'Redirect on Detect'     = $Policy.Redirect
                                'Redirect Address'       = if ($Policy.RedirectAddress) { $Policy.RedirectAddress } else { 'Not Set' }
                            }
                            $SAObj.Add([pscustomobject](ConvertTo-HashToYN $saInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.SafeAttachments) {
                                $null = ($SAObj | Where-Object { $_.'Enabled' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                                $null = ($SAObj | Where-Object { $_.'Action' -eq 'Allow' } | Set-Style -Style Critical | Out-Null)
                            }
                        })

                        $SATableParams = @{ Name = "Safe Attachments Policies - $TenantId"; List = $false; ColumnWidths = 18, 8, 8, 14, 12, 14, 12, 14 }
                        if ($Report.ShowTableCaptions) { $SATableParams['Caption'] = "- $($SATableParams.Name)" }
                        $SAObj | Table @SATableParams

                        $script:ExcelSheets['Safe Attachments'] = $SAObj

                        # SPO/ODB ATP Status
                        BlankLine
                        $ATPObj = [pscustomobject]@{
                            'Safe Attachments for SharePoint/OneDrive/Teams' = if ($SafeAttachForSPOEnabled) { 'Enabled' } else { 'Disabled' }
                        }
                        if ($HealthCheck.ExchangeOnline.SafeAttachments -and -not $SafeAttachForSPOEnabled) {
                            $ATPObj | Set-Style -Style Warning | Out-Null
                        }
                        Paragraph "Safe Attachments for SharePoint, OneDrive, and Microsoft Teams: $(if ($SafeAttachForSPOEnabled) { 'ENABLED' } else { 'DISABLED -- remediation recommended.' })"
                    } else {
                        Paragraph "No Safe Attachments policies found. Defender for Office 365 Plan 1 or higher is required."
                    }

                    # Policy Rules
                    $SAFRules = Get-SafeAttachmentRule -ErrorAction SilentlyContinue | Sort-Object Priority
                    if ($InfoLevel.SafeAttachments -ge 2 -and $SAFRules) {
                        BlankLine
                        Section -Style Heading3 'Safe Attachments Policy Assignments' {
                            Paragraph "The following assignments determine which users are protected by each Safe Attachments policy."
                            BlankLine
                            $SARuleObj = [System.Collections.ArrayList]::new()
                            foreach ($Rule in $SAFRules) {
                                $saRuleInObj = [ordered] @{
                                    'Rule Name'  = $Rule.Name
                                    'Priority'   = $Rule.Priority
                                    'State'      = $Rule.State
                                    'Policy'     = $Rule.SafeAttachmentPolicy
                                    'Applied To' = (@($Rule.SentTo) + @($Rule.SentToMemberOf) + @($Rule.RecipientDomainIs)) -join ', '
                                }
                                $SARuleObj.Add([pscustomobject]$saRuleInObj) | Out-Null
                            }
                            $null = (& {
                                if ($HealthCheck.ExchangeOnline.SafeAttachments) {
                                    $null = ($SARuleObj | Where-Object { $_.State -eq 'Disabled' } | Set-Style -Style Warning | Out-Null)
                                }
                            })
                            $SARuleTableParams = @{ Name = "Safe Attachments Assignments - $TenantId"; List = $false; ColumnWidths = 22, 8, 8, 22, 40 }
                            if ($Report.ShowTableCaptions) { $SARuleTableParams['Caption'] = "- $($SARuleTableParams.Name)" }
                            $SARuleObj | Table @SARuleTableParams
                        }
                    }
                }
            } catch {
                Write-ExoError 'SafeAttachments' "Unable to retrieve Safe Attachments policies: $($_.Exception.Message)"
                Paragraph "Safe Attachments policies could not be retrieved. This feature requires Defender for Office 365 Plan 1."
            }
            #endregion

            #region Safe Links
            try {
                Write-Host " - Retrieving Safe Links policies..."
                $SafeLinksPolicies = Get-SafeLinksPolicy -ErrorAction Stop | Sort-Object IsDefault -Descending

                $SafeLinksPolicyCount          = @($SafeLinksPolicies | Where-Object { $_.IsEnabled -eq $true -or $_.State -eq 'Enabled' }).Count
                $SafeLinksDoNotRewriteUrlCount = ($SafeLinksPolicies.DoNotRewriteUrls | Measure-Object -Sum).Count
                $SafeLinksRealTimeCount        = @($SafeLinksPolicies | Where-Object {
                    ($_.IsEnabled -eq $true -or $_.State -eq 'Enabled') -and $_.ScanUrls -eq $true
                }).Count

                Section -Style Heading3 'Safe Links Policies' {
                    if ($SafeLinksPolicies) {
                        Paragraph "$(@($SafeLinksPolicies).Count) Safe Links policy/policies found. $SafeLinksPolicyCount are enabled."
                        BlankLine

                        $SLObj = [System.Collections.ArrayList]::new()
                        foreach ($Policy in $SafeLinksPolicies) {
                            $DoNotRewriteCount = if ($Policy.DoNotRewriteUrls) { @($Policy.DoNotRewriteUrls).Count } else { 0 }

                            $slInObj = [ordered] @{
                                'Policy Name'               = $Policy.Name
                                'Enabled'                   = ($Policy.IsEnabled -eq $true -or $Policy.State -eq 'Enabled')
                                'Default Policy'            = $Policy.IsDefault
                                'Scan URLs'                 = $Policy.ScanUrls
                                'Real-time Scan'            = $Policy.DeliverMessageAfterScan
                                'Scan Internal Senders'     = $Policy.EnableForInternalSenders
                                'Track Clicks'              = $Policy.TrackClicks
                                'Allow Click-through'       = $Policy.AllowClickThrough
                                'Disable URL Rewrite'       = $Policy.DisableUrlRewrite
                                'Safe Links for Teams'      = $Policy.EnableSafeLinksForTeams
                                'Safe Links for Office'     = $Policy.EnableSafeLinksForO365Clients
                                'DoNotRewrite URL Count'    = $DoNotRewriteCount
                            }
                            $SLObj.Add([pscustomobject](ConvertTo-HashToYN $slInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.SafeLinks) {
                                $null = ($SLObj | Where-Object { $_.'Enabled' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                                $null = ($SLObj | Where-Object { $_.'Scan URLs' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                                $null = ($SLObj | Where-Object {
                                    $_.'DoNotRewrite URL Count' -ne '--' -and [int]$_.'DoNotRewrite URL Count' -gt 10
                                } | Set-Style -Style Warning | Out-Null)
                                $null = ($SLObj | Where-Object { $_.'Allow Click-through' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $SLTableParams = @{ Name = "Safe Links Policies - $TenantId"; List = $false; ColumnWidths = 14, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8 }
                        if ($Report.ShowTableCaptions) { $SLTableParams['Caption'] = "- $($SLTableParams.Name)" }
                        $SLObj | Table @SLTableParams

                        $script:ExcelSheets['Safe Links'] = $SLObj

                        # DoNotRewrite URLs detail
                        if ($InfoLevel.SafeLinks -ge 2) {
                            $AllDoNotRewrite = [System.Collections.ArrayList]::new()
                            foreach ($Policy in $SafeLinksPolicies) {
                                if ($Policy.DoNotRewriteUrls -and $Policy.DoNotRewriteUrls.Count -gt 0) {
                                    foreach ($Url in $Policy.DoNotRewriteUrls) {
                                        $null = $AllDoNotRewrite.Add([pscustomobject]@{
                                            'Policy' = $Policy.Name
                                            'Excluded URL' = $Url
                                        })
                                    }
                                }
                            }
                            if ($AllDoNotRewrite.Count -gt 0) {
                                Section -Style Heading3 'Safe Links URL Exclusions (DoNotRewriteUrls)' {
                                    Paragraph "WARNING: The following $($AllDoNotRewrite.Count) URL(s) are excluded from Safe Links scanning. Each exclusion represents a potential phishing risk."
                                    BlankLine
                                    $null = (& {
                                        if ($HealthCheck.ExchangeOnline.SafeLinks) {
                                            $null = ($AllDoNotRewrite | Set-Style -Style Warning | Out-Null)
                                        }
                                    })
                                    $DnrTableParams = @{ Name = "Safe Links URL Exclusions - $TenantId"; List = $false; ColumnWidths = 35, 65 }
                                    if ($Report.ShowTableCaptions) { $DnrTableParams['Caption'] = "- $($DnrTableParams.Name)" }
                                    $AllDoNotRewrite | Table @DnrTableParams
                                    $script:ExcelSheets['Safe Links Exclusions'] = $AllDoNotRewrite
                                }
                            }
                        }
                    } else {
                        Paragraph "No Safe Links policies found. Defender for Office 365 Plan 1 or higher is required."
                    }
                }
            } catch {
                Write-ExoError 'SafeLinks' "Unable to retrieve Safe Links policies: $($_.Exception.Message)"
                Paragraph "Safe Links policies could not be retrieved. This feature requires Defender for Office 365 Plan 1."
            }
            #endregion


            #region ACSC E8 - Safe Attachments
            BlankLine
            Paragraph "ACSC Essential Eight Assessment"
            BlankLine
            $e8VarsSA = @{
                SafeAttachPolicyCount      = $SafeAttachPolicyCount
                SafeAttachBlockActionCount = $SafeAttachBlockActionCount
                SafeAttachForSPOEnabled    = $SafeAttachForSPOEnabled
            }
            $e8ChecksSA = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoE8Checks 'SafeAttachments') -Framework 'E8' -CallerVariables $e8VarsSA
            New-AbrExoE8AssessmentTable -Checks $e8ChecksSA -Name 'Safe Attachments' -TenantId $TenantId
            if ($e8ChecksSA) {
                foreach ($row in $e8ChecksSA) {
                    $null = $script:E8AllChecks.Add([pscustomobject]@{
                        Section = 'Safe Attachments'
                        ML      = $row.ML
                        Control = $row.Control
                        Status  = $row.Status
                        Detail  = $row.Detail
                    })
                }
            }
            #endregion

            #region ACSC E8 - Safe Links
            BlankLine
            Paragraph "ACSC Essential Eight Assessment"
            BlankLine
            $e8VarsSL = @{
                SafeLinksPolicyCount          = $SafeLinksPolicyCount
                SafeLinksDoNotRewriteUrlCount = $SafeLinksDoNotRewriteUrlCount
                SafeLinksRealTimeCount        = $SafeLinksRealTimeCount
            }
            $e8ChecksSL = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoE8Checks 'SafeLinks') -Framework 'E8' -CallerVariables $e8VarsSL
            New-AbrExoE8AssessmentTable -Checks $e8ChecksSL -Name 'Safe Links' -TenantId $TenantId
            if ($e8ChecksSL) {
                foreach ($row in $e8ChecksSL) {
                    $null = $script:E8AllChecks.Add([pscustomobject]@{
                        Section = 'Safe Links'
                        ML      = $row.ML
                        Control = $row.Control
                        Status  = $row.Status
                        Detail  = $row.Detail
                    })
                }
            }
            #endregion

            #region CIS - Safe Attachments
            BlankLine
            Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment"
            BlankLine
            $cisVarsSA = @{
                SafeAttachPolicyCount   = $SafeAttachPolicyCount
                SafeAttachForSPOEnabled = $SafeAttachForSPOEnabled
            }
            $cisChecksSA = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoCISChecks 'SafeAttachments') -Framework 'CIS' -CallerVariables $cisVarsSA
            New-AbrExoCISAssessmentTable -Checks $cisChecksSA -Name 'Safe Attachments' -TenantId $TenantId
            if ($cisChecksSA) {
                foreach ($row in $cisChecksSA) {
                    $null = $script:CISAllChecks.Add([pscustomobject]@{
                        Section    = 'Safe Attachments'
                        CISControl = $row.CISControl
                        Level      = $row.Level
                        Status     = $row.Status
                        Detail     = $row.Detail
                    })
                }
            }
            #endregion

            #region CIS - Safe Links
            BlankLine
            Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment"
            BlankLine
            $cisVarsSL = @{
                SafeLinksPolicyCount          = $SafeLinksPolicyCount
                SafeLinksDoNotRewriteUrlCount = $SafeLinksDoNotRewriteUrlCount
            }
            $cisChecksSL = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoCISChecks 'SafeLinks') -Framework 'CIS' -CallerVariables $cisVarsSL
            New-AbrExoCISAssessmentTable -Checks $cisChecksSL -Name 'Safe Links' -TenantId $TenantId
            if ($cisChecksSL) {
                foreach ($row in $cisChecksSL) {
                    $null = $script:CISAllChecks.Add([pscustomobject]@{
                        Section    = 'Safe Links'
                        CISControl = $row.CISControl
                        Level      = $row.Level
                        Status     = $row.Status
                        Detail     = $row.Detail
                    })
                }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'SafeAttachmentsSafeLinks'
    }
}