tests/Test-AZSCPermissions.Tests.ps1
|
#Requires -Modules Pester <# .SYNOPSIS Pester tests for Test-AZSCPermissions. .DESCRIPTION Validates the pre-flight permission checker: - ARM subscription enumeration (pass/fail) - ARM root MG access (pass/warn) - Graph organization read (pass/fail) - Graph user read (pass/fail) - Graph conditional access read (pass/warn) - Scope gating (ArmOnly skips Graph, EntraOnly skips ARM) - Always returns structured object, never throws Tests mock Invoke-AZSCPermissionAudit (the delegated audit function) rather than individual Az cmdlets, since Test-AZSCPermissions is a pure mapping wrapper. .NOTES Author: thisismydemo Version: 1.0.0 Created: 2026-02-23 #> BeforeAll { $ModuleRoot = Split-Path -Parent $PSScriptRoot Import-Module (Join-Path $ModuleRoot 'AzureScout.psd1') -Force -ErrorAction Stop # Helper to build a mock audit result with sensible defaults function New-MockAuditResult { param( [bool]$ArmAccess = $true, $GraphAccess = $true, [array]$ArmDetails = @(), [array]$ProviderResults = @(), [array]$GraphDetails = @(), [string]$OverallReadiness = 'FullARMAndEntra' ) [PSCustomObject]@{ ArmAccess = $ArmAccess GraphAccess = $GraphAccess CallerAccount = 'test@contoso.com' CallerType = 'User' TenantId = 'test-tenant' ArmDetails = $ArmDetails ProviderResults = $ProviderResults GraphDetails = $GraphDetails Recommendations = @() OverallReadiness = $OverallReadiness AuditTimestamp = (Get-Date -Format 'o') } } function New-CheckDetail { param([string]$Check, [string]$Status = 'Pass', [string]$Message = '', [string]$Remediation = '') [PSCustomObject]@{ Check = $Check; Status = $Status; Message = $Message; Remediation = $Remediation } } } Describe 'Test-AZSCPermissions' { # ── Return Structure ─────────────────────────────────────────────── Context 'Return Structure' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' 'Found 1 subscription(s)' ) ` -GraphDetails @( New-CheckDetail 'Graph: Organization Read' 'Pass' New-CheckDetail 'Graph: Users Read' 'Pass' New-CheckDetail 'Graph: Conditional Access Read' 'Pass' ) } -ModuleName AzureScout } It 'Returns an object with ArmAccess, GraphAccess, and Details properties' { $result = Test-AZSCPermissions -TenantID '00000000-0000-0000-0000-000000000000' $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'ArmAccess' $result.PSObject.Properties.Name | Should -Contain 'GraphAccess' $result.PSObject.Properties.Name | Should -Contain 'Details' } It 'ArmAccess and GraphAccess are booleans' { $result = Test-AZSCPermissions -TenantID '00000000-0000-0000-0000-000000000000' $result.ArmAccess | Should -BeOfType [bool] $result.GraphAccess | Should -BeOfType [bool] } It 'Details is a collection' { $result = Test-AZSCPermissions -TenantID '00000000-0000-0000-0000-000000000000' $result.Details.Count | Should -BeGreaterThan 0 } } # ── ARM Checks — All Pass ───────────────────────────────────────── Context 'ARM Checks — All Pass' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $true ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' 'Found 1 subscription(s)' New-CheckDetail 'ARM: Root Management Group Access' 'Pass' 'Can read root MG' ) ` -GraphAccess $null } -ModuleName AzureScout } It 'Sets ArmAccess to $true when subscriptions are found' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $result.ArmAccess | Should -BeTrue } It 'Reports ARM: Subscription Enumeration as Pass' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $subCheck = $result.Details | Where-Object { $_.Check -eq 'ARM: Subscription Enumeration' } $subCheck.Status | Should -Be 'Pass' } It 'Reports ARM: Root Management Group Access as Pass' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $roleCheck = $result.Details | Where-Object { $_.Check -eq 'ARM: Root Management Group Access' } $roleCheck.Status | Should -Be 'Pass' } } # ── ARM Checks — No Subscriptions ───────────────────────────────── Context 'ARM Checks — No Subscriptions' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $false ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Fail' 'No subscriptions found' 'Grant Reader role' ) ` -GraphAccess $null -OverallReadiness 'Insufficient' } -ModuleName AzureScout } It 'Sets ArmAccess to $false when no subscriptions found' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $result.ArmAccess | Should -BeFalse } It 'Reports subscription enumeration as Fail' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $subCheck = $result.Details | Where-Object { $_.Check -eq 'ARM: Subscription Enumeration' } $subCheck.Status | Should -Be 'Fail' } } # ── ARM Checks — Get-AzSubscription Throws ──────────────────────── Context 'ARM Checks — Subscription Enumeration Fails' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $false ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Fail' 'Unauthorized' 'Grant Reader role on sub' ) ` -GraphAccess $null -OverallReadiness 'Insufficient' } -ModuleName AzureScout } It 'Sets ArmAccess to $false when Get-AzSubscription throws' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $result.ArmAccess | Should -BeFalse } It 'Captures the error message in Details' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $subCheck = $result.Details | Where-Object { $_.Check -eq 'ARM: Subscription Enumeration' } $subCheck.Status | Should -Be 'Fail' $subCheck.Remediation | Should -Not -BeNullOrEmpty } } # ── ARM Checks — Root MG Access Warns ───────────────────────────── Context 'ARM Checks — Root MG Access Warning' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $true ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' 'Found 1 sub' New-CheckDetail 'ARM: Root Management Group Access' 'Warn' 'Cannot read root MG' 'Grant Reader at root MG' ) ` -GraphAccess $null } -ModuleName AzureScout } It 'ArmAccess remains $true (root MG access is non-blocking)' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $result.ArmAccess | Should -BeTrue } It 'Reports root MG access as Warn' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $roleCheck = $result.Details | Where-Object { $_.Check -eq 'ARM: Root Management Group Access' } $roleCheck.Status | Should -Be 'Warn' } } # ── Graph Checks — All Pass ─────────────────────────────────────── Context 'Graph Checks — All Pass' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $true -GraphAccess $true ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' ) ` -GraphDetails @( New-CheckDetail 'Graph: Organization Read' 'Pass' New-CheckDetail 'Graph: Users Read' 'Pass' New-CheckDetail 'Graph: Groups Read' 'Pass' New-CheckDetail 'Graph: Applications Read' 'Pass' New-CheckDetail 'Graph: Service Principals Read' 'Pass' New-CheckDetail 'Graph: Directory Roles Read' 'Pass' New-CheckDetail 'Graph: Conditional Access Read' 'Pass' New-CheckDetail 'Graph: Risky Users Read' 'Pass' New-CheckDetail 'Graph: Audit Logs Read' 'Pass' ) } -ModuleName AzureScout } It 'Sets GraphAccess to $true when all Graph checks pass' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $result.GraphAccess | Should -BeTrue } It 'Has Graph detail entries' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $graphChecks = $result.Details | Where-Object { $_.Check -like 'Graph:*' } $graphChecks.Count | Should -BeGreaterOrEqual 3 } } # ── Graph Checks — Organization Read Fails ──────────────────────── Context 'Graph Checks — Organization Read Fails' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $true -GraphAccess $false ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' ) ` -GraphDetails @( New-CheckDetail 'Graph: Organization Read' 'Fail' 'DENIED' 'Grant Organization.Read.All' New-CheckDetail 'Graph: Users Read' 'Pass' New-CheckDetail 'Graph: Conditional Access Read' 'Pass' ) } -ModuleName AzureScout } It 'Sets GraphAccess to $false' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $result.GraphAccess | Should -BeFalse } It 'Reports Organization Read as Fail with remediation' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $orgCheck = $result.Details | Where-Object { $_.Check -eq 'Graph: Organization Read' } $orgCheck.Status | Should -Be 'Fail' $orgCheck.Remediation | Should -Not -BeNullOrEmpty } } # ── Graph Checks — User Read Fails ──────────────────────────────── Context 'Graph Checks — User Read Fails' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $true -GraphAccess $false ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' ) ` -GraphDetails @( New-CheckDetail 'Graph: Organization Read' 'Pass' New-CheckDetail 'Graph: Users Read' 'Fail' 'DENIED' 'Grant User.Read.All' ) } -ModuleName AzureScout } It 'Sets GraphAccess to $false when user read fails' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $result.GraphAccess | Should -BeFalse } } # ── Graph Checks — Conditional Access Warns ─────────────────────── Context 'Graph Checks — Conditional Access Warns' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $true -GraphAccess $true ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' ) ` -GraphDetails @( New-CheckDetail 'Graph: Organization Read' 'Pass' New-CheckDetail 'Graph: Users Read' 'Pass' New-CheckDetail 'Graph: Conditional Access Read' 'Warn' 'DENIED — optional' 'Grant Policy.Read.All' ) } -ModuleName AzureScout } It 'GraphAccess remains $true (CA is optional, warn-only)' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $result.GraphAccess | Should -BeTrue } It 'Reports CA policies check as Warn' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $caCheck = $result.Details | Where-Object { $_.Check -eq 'Graph: Conditional Access Read' } $caCheck.Status | Should -Be 'Warn' } } # ── Scope Gating ────────────────────────────────────────────────── Context 'Scope Gating' { BeforeAll { Mock Invoke-AZSCPermissionAudit { $gd = @() $ga = $null if ($IncludeEntraPermissions) { $gd = @( New-CheckDetail 'Graph: Organization Read' 'Pass' New-CheckDetail 'Graph: Users Read' 'Pass' ) $ga = $true } New-MockAuditResult -ArmAccess $true -GraphAccess $ga ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' New-CheckDetail 'ARM: Root Management Group Access' 'Pass' ) ` -GraphDetails $gd } -ModuleName AzureScout } It 'ArmOnly scope produces no Graph checks' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly $graphChecks = $result.Details | Where-Object { $_.Check -like 'Graph:*' } $graphChecks | Should -BeNullOrEmpty } It 'EntraOnly scope produces no ARM checks' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope EntraOnly $armChecks = $result.Details | Where-Object { $_.Check -like 'ARM:*' } $armChecks | Should -BeNullOrEmpty } It 'All scope produces both ARM and Graph checks' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope All $armChecks = $result.Details | Where-Object { $_.Check -like 'ARM:*' } $graphChecks = $result.Details | Where-Object { $_.Check -like 'Graph:*' } $armChecks.Count | Should -BeGreaterThan 0 $graphChecks.Count | Should -BeGreaterThan 0 } } # ── Subscription Scoping ───────────────────────────────────────── Context 'Subscription Scoping' { BeforeAll { Mock Invoke-AZSCPermissionAudit { New-MockAuditResult -ArmAccess $true -GraphAccess $null ` -ArmDetails @( New-CheckDetail 'ARM: Subscription Enumeration' 'Pass' 'Scoped to 1 of 5 accessible subscription(s)' ) } -ModuleName AzureScout } It 'Passes SubscriptionID through to Invoke-AZSCPermissionAudit' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -SubscriptionID 'sub-123' -Scope ArmOnly Should -InvokeVerifiable Assert-MockCalled Invoke-AZSCPermissionAudit -ModuleName AzureScout -ParameterFilter { $SubscriptionID -contains 'sub-123' } } It 'Omits SubscriptionID when not specified' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope ArmOnly Assert-MockCalled Invoke-AZSCPermissionAudit -ModuleName AzureScout -ParameterFilter { -not $SubscriptionID } } } # ── No Auth Context (Invoke-AZSCPermissionAudit returns null) ───── Context 'Never Throws' { BeforeAll { Mock Invoke-AZSCPermissionAudit { return $null } -ModuleName AzureScout } It 'Returns a result even when all checks fail' { $result = Test-AZSCPermissions -TenantID 'test-tenant' -Scope All $result | Should -Not -BeNullOrEmpty $result.ArmAccess | Should -BeFalse $result.GraphAccess | Should -BeFalse } } } |