tests/Test-Assessment.41114.ps1

<#
.SYNOPSIS
    Checks that Microsoft Teams protection policies are configured to block malicious messages, links, and attachments.
 
.NOTES
    Test ID: 41114
    Workshop Task: SECOPS-114
    Pillar: SecOps
    Category: Email and collaboration security
    Required role: Security Reader
#>


function Test-Assessment-41114 {
    [ZtTest(
        Category = 'Email and collaboration security',
        CompatibleLicense = ('ATP_ENTERPRISE'),
        ImplementationCost = 'Low',
        Pillar = 'SecOps',
        RiskLevel = 'High',
        Service = ('Graph','ExchangeOnline'),
        SfiPillar = 'Protect tenants and isolate production systems',
        TenantType = ('Workforce'),
        TestId = 41114,
        Title = 'Microsoft Teams protection policies are configured to block malicious messages, links, and attachments',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection
    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    $mgEnv = (Get-MgContext).Environment
    if ($mgEnv -eq 'USGov' -or $mgEnv -eq 'USGovDoD') {
        Write-PSFMessage 'This test is not applicable to the USGov or USGovDoD environments.' -Tag Test -Level VeryVerbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable -Result 'This test is not applicable to the USGov or USGovDoD environments.'
        return
    }

    $activity = 'Checking Microsoft Teams protection policies in Microsoft Defender for Office 365'
    Write-ZtProgress -Activity $activity -Status 'Retrieving Safe Attachments policy for SharePoint, OneDrive, and Teams'
    $safeAttachmentPolicy = $null
    $q1Error = $null
    try {
        # Q1: Retrieve Safe Attachments policy for SharePoint, OneDrive, and Teams.
        $safeAttachmentPolicy = Get-AtpPolicyForO365 -ErrorAction Stop | Select-Object Name, EnableATPForSPOTeamsODB
    }
    catch {
        $q1Error = $_
        Write-PSFMessage "Q1: Failed to retrieve Safe Attachments policy: $_" -Tag Test -Level Warning
    }

    Write-ZtProgress -Activity $activity -Status 'Retrieving Teams protection policy'
    $teamsProtectionPolicy = $null
    $q2Error = $null
    try {
        # Q2a: Retrieve Teams protection policy (ZAP and quarantine settings).
        $teamsProtectionPolicy = @(Get-TeamsProtectionPolicy -ErrorAction Stop | Select-Object Name, ZapEnabled)
    }
    catch {
        $q2Error = $_
        Write-PSFMessage "Q2: Failed to retrieve Teams protection policy: $_" -Tag Test -Level Warning
    }

    Write-ZtProgress -Activity $activity -Status 'Retrieving Teams protection policy rule'
    $teamsProtectionPolicyRule = $null
    $q2rError = $null
    try {
        # Q2b: Retrieve Teams protection rule
        $teamsProtectionPolicyRule = @(Get-TeamsProtectionPolicyRule -ErrorAction Stop |
            Select-Object Name, TeamsProtectionPolicy, State, ExceptIfSentTo, ExceptIfSentToMemberOf, ExceptIfRecipientDomainIs)
    }
    catch {
        $q2rError = $_
        Write-PSFMessage "Q2r: Failed to retrieve Teams protection policy rule: $_" -Tag Test -Level Warning
    }

    Write-ZtProgress -Activity $activity -Status 'Retrieving Safe Links policies'
    $safeLinksPolicies = $null
    try {
        # Q3a: Retrieve all Safe Links policies; IsBuiltInProtection identifies the Microsoft-managed
        # Built-in Protection preset that applies to users not covered by a Standard/Strict/custom policy.
        $safeLinksPolicies = @(Get-SafeLinksPolicy -ErrorAction Stop | Select-Object Identity, IsBuiltInProtection, IsDefault, EnableSafeLinksForTeams)
    }
    catch {
        Write-PSFMessage "Q3: Failed to retrieve Safe Links policies: $_" -Tag Test -Level Warning
    }

    Write-ZtProgress -Activity $activity -Status 'Retrieving Safe Links rules'
    $safeLinksRules = $null
    $q3rError = $null
    try {
        # Q3b: Retrieve Safe Links rules; only enabled rules determine which policies are in scope.
        $safeLinksRules = @(Get-SafeLinksRule -ErrorAction Stop | Select-Object Name, SafeLinksPolicy, State)
    }
    catch {
        $q3rError = $_
        Write-PSFMessage "Q3r: Failed to retrieve Safe Links rules: $_" -Tag Test -Level Warning
    }

    Write-ZtProgress -Activity $activity -Status 'Retrieving Defender submission policy'
    $reportSubmissionPolicy = $null
    $q4Error = $null
    try {
        # Q4: Retrieve Teams user-reporting configuration from the Defender submission policy.
        $reportSubmissionPolicy = Get-ReportSubmissionPolicy -ErrorAction Stop | Select-Object Identity, ReportChatMessageEnabled
    }
    catch {
        $q4Error = $_
        Write-PSFMessage "Q4: Failed to retrieve report submission policy: $_" -Tag Test -Level Warning
    }
    #endregion Data Collection

    #region Assessment Logic
    $anyFail        = $false
    $anyInvestigate = $false

    $controlRows = [System.Collections.Generic.List[PSCustomObject]]::new()

    $q1PortalLink = 'https://security.microsoft.com/safeattachmentv2'
    $q2PortalLink = 'https://security.microsoft.com/securitysettings/teamsProtectionPolicy'
    $q3PortalLink = 'https://security.microsoft.com/safelinksv2'
    $q4PortalLink = 'https://security.microsoft.com/securitysettings/userSubmission'

    # --- Q1: Safe Attachments for SharePoint, OneDrive, and Teams (tenant-wide) ---
    $q1Unknown = $q1Error -or $null -eq $safeAttachmentPolicy -or $null -eq $safeAttachmentPolicy.EnableATPForSPOTeamsODB
    $q1Enabled = -not $q1Unknown -and $safeAttachmentPolicy.EnableATPForSPOTeamsODB -eq $true
    if ($q1Unknown) { $anyInvestigate = $true } elseif (-not $q1Enabled) { $anyFail = $true }
    $controlRows.Add([PSCustomObject]@{
        Setting        = "[Safe Attachments for SharePoint, OneDrive, and Microsoft Teams]($q1PortalLink)"
        Policy         = ''
        AppliedViaRule = ''
        Enabled        = if ($q1Unknown) { '⚠️ Unknown' } elseif ($q1Enabled) { '✅ Yes' } else { '❌ No' }
        Status         = if ($q1Unknown) { '⚠️ Investigate' } elseif ($q1Enabled) { '✅ Pass' } else { '❌ Fail' }
    })

    # --- Q2: ZAP for Teams — the organization-wide policy determines ZAP state. ---
    # The optional Teams Protection rule only narrows coverage through exclusions.
    $q2PolicyAnomaly = $q2Error -or $null -eq $teamsProtectionPolicy -or $teamsProtectionPolicy.Count -ne 1
    if ($q2PolicyAnomaly) {
        $anyInvestigate = $true
        $controlRows.Add([PSCustomObject]@{
            Setting        = "[ZAP for Teams]($q2PortalLink)"
            Policy         = ''
            AppliedViaRule = ''
            Enabled        = '⚠️ Unknown'
            Status         = '⚠️ Investigate — could not retrieve the organization-wide Teams Protection policy'
        })
    }
    else {
        $organizationTeamsPolicy = $teamsProtectionPolicy[0]
        $zapUnknown = $null -eq $organizationTeamsPolicy.ZapEnabled
        $zapEnabled = -not $zapUnknown -and $organizationTeamsPolicy.ZapEnabled -eq $true
        if ($zapUnknown) {
            $anyInvestigate = $true
            $zapStatus = '⚠️ Investigate — ZapEnabled value is ambiguous'
        }
        elseif (-not $zapEnabled) {
            $anyFail = $true
            $zapStatus = '❌ Fail'
        }
        else {
            $zapStatus = '✅ Pass'
        }

        $enabledTeamsRule = $null
        $hasExceptions = $false
        $exceptionProperties = @()
        if ($q2rError) {
            if (-not $anyFail) {
                $anyInvestigate = $true
                $zapStatus = '⚠️ Investigate — could not retrieve Teams Protection rules to check exclusions'
            }
        }
        else {
            $teamsProtectionRule = $teamsProtectionPolicyRule | Select-Object -First 1
            if ($teamsProtectionRule.State -eq 'Enabled') {
                $enabledTeamsRule = $teamsProtectionRule
                if ($teamsProtectionRule.ExceptIfSentTo) {
                    $exceptionProperties += 'ExceptIfSentTo'
                }
                if ($teamsProtectionRule.ExceptIfSentToMemberOf) {
                    $exceptionProperties += 'ExceptIfSentToMemberOf'
                }
                if ($teamsProtectionRule.ExceptIfRecipientDomainIs) {
                    $exceptionProperties += 'ExceptIfRecipientDomainIs'
                }
                $hasExceptions = $exceptionProperties.Count -gt 0
            }

            if ($hasExceptions -and -not $anyFail) {
                $anyInvestigate = $true
                $zapStatus = "⚠️ Investigate — $($exceptionProperties -join ', ') exceptions reduce ZAP coverage"
            }
        }

        $controlRows.Add([PSCustomObject]@{
            Setting        = "[ZAP for Teams]($q2PortalLink)"
            Policy         = $organizationTeamsPolicy.Name
            AppliedViaRule = $enabledTeamsRule.Name
            Enabled        = if ($zapUnknown) { '⚠️ Unknown' } elseif ($zapEnabled) { '✅ Yes' } else { '❌ No' }
            Status         = $zapStatus
        })
    }

    # --- Q3: Safe Links for Teams — evaluated per enabled Safe Links rule/policy ---
    # Build lookup: policy Identity → policy object.
    $safeLinksPolicyByIdentity = @{}
    if ($safeLinksPolicies) {
        foreach ($p in $safeLinksPolicies) {
            $safeLinksPolicyByIdentity[$p.Identity] = $p
        }
    }

    # Zero or null $safeLinksPolicies is a retrieval/permission anomaly: every MDO P1 tenant has at
    # least Built-in Protection, so an empty result should not silently become a Fail verdict.
    $q3PolicyAnomaly = $null -eq $safeLinksPolicies -or $safeLinksPolicies.Count -eq 0

    if ($q3PolicyAnomaly) {
        # No Safe Links policies were retrieved, so no effective policy state is known.
        $anyInvestigate = $true
        $controlRows.Add([PSCustomObject]@{
            Setting        = "[Safe Links for Teams]($q3PortalLink)"
            Policy         = ''
            AppliedViaRule = ''
            Enabled        = '⚠️ Unknown'
            Status         = '⚠️ Investigate — could not retrieve Safe Links policies'
        })
    }
    else {
        # Built-in Protection covers recipients not matched by an enabled custom rule, so it is
        # always effective and must be reported alongside enabled-rule-referenced policies.
        $builtInSafeLinksPolicy = $safeLinksPolicies | Where-Object { $_.IsBuiltInProtection -eq $true } | Select-Object -First 1
        if ($null -eq $builtInSafeLinksPolicy) {
            $anyInvestigate = $true
            $controlRows.Add([PSCustomObject]@{
                Setting        = "[Safe Links for Teams]($q3PortalLink)"
                Policy         = ''
                AppliedViaRule = ''
                Enabled        = '⚠️ Unknown'
                Status         = '⚠️ Investigate — Built-in Protection policy not found'
            })
        }
        else {
            $teamsUnknown = $null -eq $builtInSafeLinksPolicy.EnableSafeLinksForTeams
            $teamsEnabled = -not $teamsUnknown -and $builtInSafeLinksPolicy.EnableSafeLinksForTeams -eq $true
            if ($teamsUnknown) { $anyInvestigate = $true } elseif (-not $teamsEnabled) { $anyFail = $true }
            $controlRows.Add([PSCustomObject]@{
                Setting        = "[Safe Links for Teams]($q3PortalLink)"
                Policy         = "(Built-in) $($builtInSafeLinksPolicy.Identity)"
                AppliedViaRule = ''
                Enabled        = if ($teamsUnknown) { '⚠️ Unknown' } elseif ($teamsEnabled) { '✅ Yes' } else { '❌ No' }
                Status         = if ($teamsUnknown) { '⚠️ Investigate' } elseif ($teamsEnabled) { '✅ Pass' } else { '❌ Fail' }
            })
        }

        if ($q3rError) {
            # Built-in state is known, but custom/preset policy assignments cannot be evaluated.
            $anyInvestigate = $true
            $controlRows.Add([PSCustomObject]@{
                Setting        = "[Safe Links for Teams]($q3PortalLink)"
                Policy         = ''
                AppliedViaRule = ''
                Enabled        = '⚠️ Unknown'
                Status         = '⚠️ Investigate — could not retrieve Safe Links rules'
            })
        }
        else {
            $ambiguousSafeLinksRules = @($safeLinksRules | Where-Object { $_.State -notin @('Enabled', 'Disabled') })
            foreach ($rule in $ambiguousSafeLinksRules) {
                $anyInvestigate = $true
                $controlRows.Add([PSCustomObject]@{
                    Setting        = "[Safe Links for Teams]($q3PortalLink)"
                    Policy         = $rule.SafeLinksPolicy
                    AppliedViaRule = $rule.Name
                    Enabled        = '⚠️ Unknown'
                    Status         = '⚠️ Investigate — Safe Links rule state is ambiguous'
                })
            }

            $enabledSafeLinksRules = @($safeLinksRules | Where-Object { $_.State -eq 'Enabled' })
            foreach ($rule in $enabledSafeLinksRules) {
                $policyIdentity = $rule.SafeLinksPolicy
                $policy         = $safeLinksPolicyByIdentity[$policyIdentity]

                if ($null -eq $policy) {
                    $anyInvestigate = $true
                    $controlRows.Add([PSCustomObject]@{
                        Setting        = "[Safe Links for Teams]($q3PortalLink)"
                        Policy         = $policyIdentity
                        AppliedViaRule = $rule.Name
                        Enabled        = '⚠️ Unknown'
                        Status         = '⚠️ Investigate — referenced policy not found'
                    })
                    continue
                }

                $teamsUnknown = $null -eq $policy.EnableSafeLinksForTeams
                $teamsEnabled = -not $teamsUnknown -and $policy.EnableSafeLinksForTeams -eq $true
                if ($teamsUnknown) { $anyInvestigate = $true } elseif (-not $teamsEnabled) { $anyFail = $true }

                $controlRows.Add([PSCustomObject]@{
                    Setting        = "[Safe Links for Teams]($q3PortalLink)"
                    Policy         = $policyIdentity
                    AppliedViaRule = $rule.Name
                    Enabled        = if ($teamsUnknown) { '⚠️ Unknown' } elseif ($teamsEnabled) { '✅ Yes' } else { '❌ No' }
                    Status         = if ($teamsUnknown) { '⚠️ Investigate' } elseif ($teamsEnabled) { '✅ Pass' } else { '❌ Fail' }
                })
            }
        }
    }

    # --- Q4: Monitor reported items in Microsoft Teams (tenant-wide) ---
    $q4Unknown = $q4Error -or $null -eq $reportSubmissionPolicy -or $null -eq $reportSubmissionPolicy.ReportChatMessageEnabled
    $q4Enabled = -not $q4Unknown -and $reportSubmissionPolicy.ReportChatMessageEnabled -eq $true
    if ($q4Unknown) { $anyInvestigate = $true } elseif (-not $q4Enabled) { $anyFail = $true }
    $controlRows.Add([PSCustomObject]@{
        Setting        = "[Monitor reported items in Microsoft Teams]($q4PortalLink)"
        Policy         = ''
        AppliedViaRule = ''
        Enabled        = if ($q4Unknown) { '⚠️ Unknown' } elseif ($q4Enabled) { '✅ Yes' } else { '❌ No' }
        Status         = if ($q4Unknown) { '⚠️ Investigate' } elseif ($q4Enabled) { '✅ Pass' } else { '❌ Fail' }
    })

    # Final verdict — Fail takes priority over Investigate (spec ordering: Skipped → Fail → Investigate → Pass).
    $passed       = $false
    $customStatus = $null
    if ($anyFail) {
        $testResultMarkdown = "❌ One or more Teams protection controls is explicitly disabled, leaving report monitoring, link scanning, file detonation, or post-delivery quarantine coverage incomplete.`n`n%TestResult%"
    }
    elseif ($anyInvestigate) {
        $customStatus       = 'Investigate'
        $testResultMarkdown = "⚠️ An enabled rule references a missing policy, an enabled Teams Protection rule has exceptions, or the tenant cloud/licensing needs manual confirmation.`n`n%TestResult%"
    }
    else {
        $passed             = $true
        $testResultMarkdown = "✅ Microsoft Teams protection is configured for messages, links, and attachments: Safe Attachments, Safe Links for Teams, ZAP for Teams, and Defender submission monitoring for Teams reports are enabled on every policy in use.`n`n%TestResult%"
    }
    #endregion Assessment Logic

    #region Report Generation
    $tableRows = ''
    foreach ($row in $controlRows) {
        $policyMd         = if ($row.Policy) { Get-SafeMarkdown $row.Policy } else { '' }
        $appliedViaRuleMd = if ($row.AppliedViaRule) { Get-SafeMarkdown $row.AppliedViaRule } else { '' }
        $tableRows += "| $($row.Setting) | $policyMd | $appliedViaRuleMd | $($row.Enabled) | $($row.Status) |`n"
    }

    $formatTemplate = @'
| Setting | Policy | Applied via Rule | Enabled | Status |
| :------ | :----- | :--------------- | :------ | :----- |
{0}
'@


    $mdInfo             = $formatTemplate -f $tableRows
    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
    #endregion Report Generation

    $params = @{
        TestId = '41114'
        Title  = 'Microsoft Teams protection policies are configured to block malicious messages, links, and attachments'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }
    Add-ZtTestResultDetail @params
}