Tests/Unit/HighSeverityBugFixes.Tests.ps1
|
BeforeAll { Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue Import-Module (Join-Path $PSScriptRoot '..' '..' 'Devolutions.CIEM.psd1') Mock -ModuleName Devolutions.CIEM Write-CIEMLog {} # Create isolated test DB with base + graph schemas New-CIEMDatabase -Path "$TestDrive/ciem.db" $graphSchema = Join-Path $PSScriptRoot '..' '..' 'modules' 'Devolutions.CIEM.Graph' 'Data' 'graph_schema.sql' Invoke-CIEMQuery -Query (Get-Content $graphSchema -Raw) InModuleScope Devolutions.CIEM { $script:DatabasePath = "$TestDrive/ciem.db" } } # ============================================================================= # Issue 1: $script:DormantPermissionThresholdDays null guard # ============================================================================= Describe 'Issue 1: DormantPermissionThresholdDays null guard' { Context 'Get-CIEMIdentityRiskSignals throws when DormantPermissionThresholdDays is null' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" # Seed a privileged user so the dormant check path is reached Save-CIEMGraphNode -Id 'user-nullguard' -Kind 'EntraUser' -DisplayName 'Null Guard Test' -Provider 'azure' ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 120 lastSignIn = '2025-11-01T12:00:00Z' } | ConvertTo-Json -Compress) Save-CIEMGraphNode -Id '/subscriptions/sub-ng' -Kind 'AzureSubscription' -DisplayName 'Sub NG' -Provider 'azure' Save-CIEMGraphEdge -SourceId 'user-nullguard' -TargetId '/subscriptions/sub-ng' -Kind 'HasRole' -Computed 1 ` -Properties (@{ role_name = 'Owner'; privileged = $true; scope = '/subscriptions/sub-ng' } | ConvertTo-Json -Compress) } It 'Throws a descriptive error when $script:DormantPermissionThresholdDays is null' { InModuleScope Devolutions.CIEM { $saved = $script:DormantPermissionThresholdDays try { $script:DormantPermissionThresholdDays = $null { Get-CIEMIdentityRiskSignals -PrincipalId 'user-nullguard' } | Should -Throw '*DormantPermissionThresholdDays*' } finally { $script:DormantPermissionThresholdDays = $saved } } } } Context 'Get-CIEMIdentityRiskSummary throws when DormantPermissionThresholdDays is null' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" Save-CIEMGraphNode -Id 'user-nullguard-sum' -Kind 'EntraUser' -DisplayName 'Null Guard Summary' ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 120 } | ConvertTo-Json -Compress) Save-CIEMGraphNode -Id '/subscriptions/sub-ngs' -Kind 'AzureSubscription' -DisplayName 'Sub NGS' Save-CIEMGraphEdge -SourceId 'user-nullguard-sum' -TargetId '/subscriptions/sub-ngs' -Kind 'HasRole' ` -Properties (@{ role_name = 'Owner'; privileged = $true; scope = '/subscriptions/sub-ngs' } | ConvertTo-Json -Compress) } It 'Throws a descriptive error when $script:DormantPermissionThresholdDays is null' { InModuleScope Devolutions.CIEM { $saved = $script:DormantPermissionThresholdDays try { $script:DormantPermissionThresholdDays = $null { Get-CIEMIdentityRiskSummary } | Should -Throw '*DormantPermissionThresholdDays*' } finally { $script:DormantPermissionThresholdDays = $saved } } } } Context 'Get-CIEMIdentityRiskSummary throws when MediumEntitlementThreshold is null' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" Save-CIEMGraphNode -Id 'user-nullguard-med' -Kind 'EntraUser' -DisplayName 'Null Guard Medium' ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 5 } | ConvertTo-Json -Compress) Save-CIEMGraphNode -Id '/subscriptions/sub-ngm' -Kind 'AzureSubscription' -DisplayName 'Sub NGM' # Non-privileged role so we reach the Medium threshold check Save-CIEMGraphEdge -SourceId 'user-nullguard-med' -TargetId '/subscriptions/sub-ngm' -Kind 'HasRole' ` -Properties (@{ role_name = 'Reader'; privileged = $false; scope = '/subscriptions/sub-ngm' } | ConvertTo-Json -Compress) } It 'Throws a descriptive error when $script:MediumEntitlementThreshold is null' { InModuleScope Devolutions.CIEM { $saved = $script:MediumEntitlementThreshold try { $script:MediumEntitlementThreshold = $null { Get-CIEMIdentityRiskSummary } | Should -Throw '*MediumEntitlementThreshold*' } finally { $script:MediumEntitlementThreshold = $saved } } } } } # ============================================================================= # Issue 2: Empty catch {} in HasManagedIdentity section # ============================================================================= Describe 'Issue 2: HasManagedIdentity edge build logs errors instead of swallowing' { Context 'When Identity JSON parse fails for a resource' { It 'Calls Write-CIEMLog with a warning when HasManagedIdentity edge build fails' { InModuleScope Devolutions.CIEM { # We need to test that the catch block logs a warning instead of silently swallowing # Read the source file and check it does NOT have an empty catch $sourceFile = Join-Path $PSScriptRoot '..' '..' 'modules' 'Azure' 'Discovery' 'Private' 'InvokeCIEMGraphComputedEdgeBuild.ps1' $content = Get-Content $sourceFile -Raw # The source should NOT have a bare 'catch { }' or 'catch {}' (empty catch block) $content | Should -Not -Match 'catch\s*\{\s*\}' } } } } # ============================================================================= # Issue 3: InvokeCIEMTransaction missing $ErrorActionPreference = 'Stop' # ============================================================================= Describe 'Issue 3: InvokeCIEMTransaction has ErrorActionPreference Stop' { Context 'Source code validation' { It 'Contains $ErrorActionPreference = Stop after param block' { InModuleScope Devolutions.CIEM { $sourceFile = Join-Path $PSScriptRoot '..' '..' 'Private' 'InvokeCIEMTransaction.ps1' $content = Get-Content $sourceFile -Raw $content | Should -Match '\$ErrorActionPreference\s*=\s*[''"]Stop[''"]' } } } } # ============================================================================= # Issue 4: ResolveCIEMAttackPathFilter [double] cast crash on non-numeric # ============================================================================= Describe 'Issue 4: ResolveCIEMAttackPathFilter safe numeric parsing' { Context 'gt operator with non-numeric property value' { It 'Returns false instead of throwing when property value is non-numeric' { InModuleScope Devolutions.CIEM { $json = '{"someField":"not-a-number"}' $filter = [PSCustomObject]@{ property = 'someField'; op = 'gt'; value = 90 } $result = ResolveCIEMAttackPathFilter -PropertiesJson $json -Filter $filter $result | Should -BeFalse } } } Context 'lt operator with non-numeric property value' { It 'Returns false instead of throwing when property value is non-numeric' { InModuleScope Devolutions.CIEM { $json = '{"someField":"abc"}' $filter = [PSCustomObject]@{ property = 'someField'; op = 'lt'; value = 5 } $result = ResolveCIEMAttackPathFilter -PropertiesJson $json -Filter $filter $result | Should -BeFalse } } } Context 'gt_or_null operator with non-numeric property value' { It 'Returns false instead of throwing when property value is non-numeric' { InModuleScope Devolutions.CIEM { $json = '{"someField":"xyz"}' $filter = [PSCustomObject]@{ property = 'someField'; op = 'gt_or_null'; value = 90 } $result = ResolveCIEMAttackPathFilter -PropertiesJson $json -Filter $filter $result | Should -BeFalse } } It 'Still returns true when property value is null' { InModuleScope Devolutions.CIEM { $json = '{"otherField":"value"}' $filter = [PSCustomObject]@{ property = 'someField'; op = 'gt_or_null'; value = 90 } $result = ResolveCIEMAttackPathFilter -PropertiesJson $json -Filter $filter $result | Should -BeTrue } } } Context 'gt operator with non-numeric filter value' { It 'Returns false instead of throwing when filter value is non-numeric' { InModuleScope Devolutions.CIEM { $json = '{"daysSinceSignIn":120}' $filter = [PSCustomObject]@{ property = 'daysSinceSignIn'; op = 'gt'; value = 'not-a-number' } $result = ResolveCIEMAttackPathFilter -PropertiesJson $json -Filter $filter $result | Should -BeFalse } } } Context 'gt operator still works for valid numeric values' { It 'Returns true when numeric comparison succeeds' { InModuleScope Devolutions.CIEM { $json = '{"daysSinceSignIn":120}' $filter = [PSCustomObject]@{ property = 'daysSinceSignIn'; op = 'gt'; value = 90 } $result = ResolveCIEMAttackPathFilter -PropertiesJson $json -Filter $filter $result | Should -BeTrue } } } } |