tests/Get-MsecPowerPlatformEnvironment.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecPowerPlatformEnvironment.
#
# The trap this exists for: DLP SCOPE IS A FILTER TYPE, NOT A LIST. A connector policy carries
# environmentFilterType of 'none' (every environment), 'include' (only the listed ones) or
# 'exclude' (all but the listed ones). Reading only .environments would report a tenant-wide
# policy - filterType 'none', empty list - as covering nothing, which is the most dangerous way
# to be wrong here: it turns a protected tenant into a page of false findings, and the real
# uncovered environments get lost in them.
#
# And the usual one: policies unreadable must leave IsCoveredByDlp null, never false.

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

    $script:StubbedAz = @()
    $azStubs = @{
        'Get-AzContext'     = { [CmdletBinding()] param() }
        'Get-AzAccessToken' = { [CmdletBinding()] param($ResourceUrl) }
    }
    foreach ($c in $azStubs.Keys) {
        if (-not (Get-Command $c -ErrorAction SilentlyContinue)) {
            Set-Item "function:global:$c" -Value $azStubs[$c] -Force
            $script:StubbedAz += $c
        }
    }
}
AfterAll {
    foreach ($c in $script:StubbedAz) { Remove-Item "function:global:$c" -ErrorAction SilentlyContinue }
    Remove-Module msec -Force -ErrorAction SilentlyContinue
}

Describe 'Get-MsecPowerPlatformEnvironment' {

    BeforeEach {
        InModuleScope msec {
            Mock Get-AzContext     -MockWith { [pscustomobject]@{ Account = 'me' } }
            Mock Get-AzAccessToken -MockWith { [pscustomobject]@{ Token = 'tok' } }
        }
    }

    It "treats environmentFilterType 'none' as covering every environment" {
        $rows = InModuleScope msec {
            Mock Invoke-RestMethod -MockWith {
                if ("$Uri" -like '*scopes/admin/environments*') {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ name = 'env-1'; location = 'europe'
                                           properties = [pscustomobject]@{ displayName = 'Default'; isDefault = $true; environmentSku = 'Default' } }) }
                }
                else {
                    # Tenant-wide policy: filter type 'none' and an EMPTY environment list.
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ displayName = 'Tenant DLP'
                                           environments = [pscustomobject]@{ environmentFilterType = 'none'; environments = @() } }) }
                }
            }
            Get-MsecPowerPlatformEnvironment
        }

        $rows[0].IsCoveredByDlp | Should -BeTrue
        $rows[0].DlpPolicies | Should -Be 'Tenant DLP'
    }

    It "applies an 'include' policy only to the listed environments" {
        $rows = InModuleScope msec {
            Mock Invoke-RestMethod -MockWith {
                if ("$Uri" -like '*scopes/admin/environments*') {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ name = 'env-1'; properties = [pscustomobject]@{ displayName = 'Listed' } }
                        [pscustomobject]@{ name = 'env-2'; properties = [pscustomobject]@{ displayName = 'Not listed' } }) }
                }
                else {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ displayName = 'Scoped DLP'
                                           environments = [pscustomobject]@{ environmentFilterType = 'include'
                                                                             environments = @([pscustomobject]@{ name = 'env-1' }) } }) }
                }
            }
            Get-MsecPowerPlatformEnvironment
        }

        ($rows | Where-Object DisplayName -eq 'Listed').IsCoveredByDlp     | Should -BeTrue
        ($rows | Where-Object DisplayName -eq 'Not listed').IsCoveredByDlp | Should -BeFalse
    }

    It "applies an 'exclude' policy to everything except the listed environments" {
        $rows = InModuleScope msec {
            Mock Invoke-RestMethod -MockWith {
                if ("$Uri" -like '*scopes/admin/environments*') {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ name = 'env-1'; properties = [pscustomobject]@{ displayName = 'Carved out' } }
                        [pscustomobject]@{ name = 'env-2'; properties = [pscustomobject]@{ displayName = 'Everything else' } }) }
                }
                else {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ displayName = 'All but one'
                                           environments = [pscustomobject]@{ environmentFilterType = 'exclude'
                                                                             environments = @([pscustomobject]@{ name = 'env-1' }) } }) }
                }
            }
            Get-MsecPowerPlatformEnvironment
        }

        ($rows | Where-Object DisplayName -eq 'Carved out').IsCoveredByDlp      | Should -BeFalse
        ($rows | Where-Object DisplayName -eq 'Everything else').IsCoveredByDlp | Should -BeTrue
    }

    It 'reports IsCoveredByDlp as null when the policy list cannot be read' {
        $rows = InModuleScope msec {
            Mock Invoke-RestMethod -MockWith {
                if ("$Uri" -like '*scopes/admin/environments*') {
                    [pscustomobject]@{ value = @([pscustomobject]@{ name = 'env-1'; properties = [pscustomobject]@{ displayName = 'Env' } }) }
                }
                else { throw 'Forbidden' }
            }
            Get-MsecPowerPlatformEnvironment -WarningAction SilentlyContinue
        }

        # $false would state the environment is definitely unprotected. Nothing established that.
        $rows[0].IsCoveredByDlp | Should -BeNullOrEmpty
        $rows[0].DlpPolicyCount | Should -BeNullOrEmpty
    }

    It 'does not assume an unrecognised filter type is harmless' {
        $rows = InModuleScope msec {
            Mock Invoke-RestMethod -MockWith {
                if ("$Uri" -like '*scopes/admin/environments*') {
                    [pscustomobject]@{ value = @([pscustomobject]@{ name = 'env-1'; properties = [pscustomobject]@{ displayName = 'Env' } }) }
                }
                else {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ displayName = 'Future policy'
                                           environments = [pscustomobject]@{ environmentFilterType = 'somethingNew'; environments = @() } }) }
                }
            }
            Get-MsecPowerPlatformEnvironment
        }

        # The policy is not counted as covering, so the environment still reads uncovered
        # rather than being silently credited with protection msec does not understand.
        $rows[0].IsCoveredByDlp | Should -BeFalse
    }

    It 'returns only uncovered environments with -UncoveredOnly' {
        $rows = InModuleScope msec {
            Mock Invoke-RestMethod -MockWith {
                if ("$Uri" -like '*scopes/admin/environments*') {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ name = 'env-1'; properties = [pscustomobject]@{ displayName = 'Covered' } }
                        [pscustomobject]@{ name = 'env-2'; properties = [pscustomobject]@{ displayName = 'Open' } }) }
                }
                else {
                    [pscustomobject]@{ value = @(
                        [pscustomobject]@{ displayName = 'DLP'
                                           environments = [pscustomobject]@{ environmentFilterType = 'include'
                                                                             environments = @([pscustomobject]@{ name = 'env-1' }) } }) }
                }
            }
            Get-MsecPowerPlatformEnvironment -UncoveredOnly
        }

        $rows.Count | Should -Be 1
        $rows[0].DisplayName | Should -Be 'Open'
    }

    It 'names the role needed when Power Platform answers 403' {
        InModuleScope msec {
            Mock Invoke-RestMethod -MockWith {
                $response = [pscustomobject]@{ StatusCode = 403 }
                $ex = [System.Exception]::new('Forbidden')
                Add-Member -InputObject $ex -NotePropertyName Response -NotePropertyValue $response -Force
                throw $ex
            }
            # A bare 'Forbidden' sends the reader looking at app permissions, which are not
            # involved at all - this command runs as the user.
            { Get-MsecPowerPlatformEnvironment } | Should -Throw '*Power Platform Administrator*'
        }
    }
}