tests/Get-MsecDefenderOfficePolicy.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecDefenderOfficePolicy.
#
# In Exchange Online Protection a policy and the rule that applies it are separate objects, so
# "does this policy do anything" is a real question with four different right answers. The traps:
#
# A PRESET POLICY HAS NO RULE OF ITS OWN and is applied by the EOP/ATP protection policy rule.
# Reporting 'Strict Preset Security Policy' as unapplied because Get-AntiPhishRule does not
# mention it would cry wolf on the configuration Microsoft most recommends - and on a tenant
# that uses presets, that is nearly every policy.
#
# A DEFAULT POLICY HAS NO RULE EITHER, for a different reason: it is the fallback.
#
# RULES UNREADABLE IS NOT RULES ABSENT. If the *Rule cmdlet fails, every custom policy would
# otherwise be reported inert, which is both wrong and alarming.
#
# The stubs carry [CmdletBinding()] because the command calls them with -ErrorAction Stop, and a
# simple function has no common parameters - it would fail on the parameter, not on the data.

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop

    foreach ($name in 'Get-AntiPhishPolicy','Get-AntiPhishRule','Get-SafeLinksPolicy','Get-SafeLinksRule',
                      'Get-SafeAttachmentPolicy','Get-SafeAttachmentRule','Get-HostedContentFilterPolicy',
                      'Get-HostedContentFilterRule','Get-MalwareFilterPolicy','Get-MalwareFilterRule',
                      'Get-HostedOutboundSpamFilterPolicy','Get-HostedOutboundSpamFilterRule',
                      'Get-EOPProtectionPolicyRule','Get-ATPProtectionPolicyRule',
                      'Get-PhishSimOverridePolicy','Get-SecOpsOverridePolicy') {
        Set-Item "function:global:$name" -Value ([scriptblock]::Create('[CmdletBinding()] param()'))
    }
}
AfterAll {
    Remove-Module msec -Force -ErrorAction SilentlyContinue
    foreach ($name in 'Get-AntiPhishPolicy','Get-AntiPhishRule','Get-SafeLinksPolicy','Get-SafeLinksRule',
                      'Get-SafeAttachmentPolicy','Get-SafeAttachmentRule','Get-HostedContentFilterPolicy',
                      'Get-HostedContentFilterRule','Get-MalwareFilterPolicy','Get-MalwareFilterRule',
                      'Get-HostedOutboundSpamFilterPolicy','Get-HostedOutboundSpamFilterRule',
                      'Get-EOPProtectionPolicyRule','Get-ATPProtectionPolicyRule',
                      'Get-PhishSimOverridePolicy','Get-SecOpsOverridePolicy') {
        Remove-Item "function:global:$name" -ErrorAction SilentlyContinue
    }
}

Describe 'Get-MsecDefenderOfficePolicy' {

    It 'does not report a preset policy as unapplied just because no AntiPhishRule names it' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith {
                @([pscustomobject]@{ Name = 'Strict Preset Security Policy'; State = 'Enabled'; Priority = 0
                                     AntiPhishPolicy = 'Strict Preset Security Policy'
                                     SentToMemberOf = @('ITOperations@contoso.com') })
            }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            Mock Get-AntiPhishPolicy -MockWith {
                @([pscustomobject]@{ Name = 'Strict Preset Security Policy'; IsDefault = $false; Enabled = $true })
            }
            Mock Get-AntiPhishRule -MockWith { @() }
            Get-MsecDefenderOfficePolicy -PolicyType AntiPhish
        }

        $rows | Should -Not -BeNullOrEmpty
        $rows[0].IsApplied | Should -BeTrue
        $rows[0].AppliedBy | Should -Match '^Preset: Strict Preset Security Policy'
        $rows[0].AppliedTo | Should -Match 'ITOperations@contoso.com'
    }

    It 'treats a default policy as applied, not as a policy nobody references' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith { @() }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            Mock Get-AntiPhishPolicy -MockWith {
                @([pscustomobject]@{ Name = 'Office365 AntiPhish Default'; IsDefault = $true; Enabled = $true })
            }
            Mock Get-AntiPhishRule -MockWith { @() }
            Get-MsecDefenderOfficePolicy -PolicyType AntiPhish
        }

        $rows[0].IsApplied | Should -BeTrue
        $rows[0].AppliedBy | Should -Be 'Default'
    }

    It 'reports a custom policy no rule references as unapplied' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith { @() }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            Mock Get-AntiPhishPolicy -MockWith {
                @([pscustomobject]@{ Name = 'Carefully Written And Inert'; IsDefault = $false; Enabled = $true })
            }
            Mock Get-AntiPhishRule -MockWith { @() }
            Get-MsecDefenderOfficePolicy -PolicyType AntiPhish
        }

        $rows[0].IsApplied | Should -BeFalse
        $rows[0].AppliedBy | Should -BeNullOrEmpty
    }

    It 'reports a custom policy whose rule is disabled as unapplied, but names the rule' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith { @() }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            Mock Get-AntiPhishPolicy -MockWith {
                @([pscustomobject]@{ Name = 'Pilot'; IsDefault = $false; Enabled = $true })
            }
            Mock Get-AntiPhishRule -MockWith {
                @([pscustomobject]@{ Name = 'Pilot rule'; State = 'Disabled'; AntiPhishPolicy = 'Pilot'
                                     SentTo = @('a@contoso.com') })
            }
            Get-MsecDefenderOfficePolicy -PolicyType AntiPhish
        }

        $rows[0].IsApplied | Should -BeFalse
        $rows[0].AppliedBy | Should -Be 'Rule: Pilot rule'
        $rows[0].AppliedTo | Should -Match 'a@contoso.com'
    }

    It 'leaves IsApplied null when the rules could not be read at all' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith { @() }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            Mock Get-AntiPhishPolicy -MockWith {
                @([pscustomobject]@{ Name = 'Custom'; IsDefault = $false; Enabled = $true })
            }
            Mock Get-AntiPhishRule -MockWith { throw 'Access denied' }
            Get-MsecDefenderOfficePolicy -PolicyType AntiPhish -WarningAction SilentlyContinue
        }

        # $false would assert the policy was checked and found inert.
        $rows[0].IsApplied | Should -BeNullOrEmpty
    }

    It 'reports a policy area it could not read instead of omitting it' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith { @() }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            Mock Get-AntiPhishPolicy -MockWith { throw 'Insufficient privileges' }
            Mock Get-AntiPhishRule -MockWith { @() }
            Get-MsecDefenderOfficePolicy -PolicyType AntiPhish -WarningAction SilentlyContinue
        }

        $rows.Count | Should -Be 1
        $rows[0].PolicyName | Should -Be 'Unreadable'
        $rows[0].IsApplied | Should -BeNullOrEmpty
    }

    It 'reports advanced delivery as unreadable rather than as not configured' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith { @() }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            # This is the real failure on an app-only session.
            Mock Get-PhishSimOverridePolicy -MockWith { throw 'A server side error has occurred' }
            Mock Get-SecOpsOverridePolicy -MockWith { @() }
            Get-MsecDefenderOfficePolicy -PolicyType AdvancedDelivery -WarningAction SilentlyContinue
        }

        $phishSim = $rows | Where-Object PolicyName -match 'Phishing simulation'
        $phishSim.IsApplied | Should -BeNullOrEmpty    # unreadable
        $secOps = $rows | Where-Object PolicyName -match 'SecOps'
        $secOps.Value | Should -Be 'False'             # genuinely read, genuinely absent
        $secOps.IsApplied | Should -BeFalse
    }

    It 'returns only inert policies with -UnappliedOnly' {
        $rows = InModuleScope msec {
            Mock Initialize-MsecExoSession -MockWith { }
            Mock Get-EOPProtectionPolicyRule -MockWith { @() }
            Mock Get-ATPProtectionPolicyRule -MockWith { @() }
            Mock Get-AntiPhishPolicy -MockWith {
                @(
                    [pscustomobject]@{ Name = 'Office365 AntiPhish Default'; IsDefault = $true; Enabled = $true }
                    [pscustomobject]@{ Name = 'Inert'; IsDefault = $false; Enabled = $true }
                )
            }
            Mock Get-AntiPhishRule -MockWith { @() }
            Get-MsecDefenderOfficePolicy -PolicyType AntiPhish -UnappliedOnly
        }

        @($rows | Select-Object -ExpandProperty PolicyName -Unique) | Should -Be 'Inert'
    }
}