modules/Devolutions.CIEM.Graph/Tests/Unit/CIEMIdentityRiskSummary.Tests.ps1
|
BeforeAll { Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue Import-Module (Join-Path $PSScriptRoot '..' '..' '..' '..' 'Devolutions.CIEM.psd1') Mock -ModuleName Devolutions.CIEM Write-CIEMLog {} New-CIEMDatabase -Path "$TestDrive/ciem.db" $graphSchema = Join-Path $PSScriptRoot '..' '..' 'Data' 'graph_schema.sql' Invoke-CIEMQuery -Query (Get-Content $graphSchema -Raw) InModuleScope Devolutions.CIEM { $script:DatabasePath = "$TestDrive/ciem.db" } } Describe 'Get-CIEMIdentityRiskSummary' { Context 'Command structure' { It 'Is available as a public command' { Get-Command Get-CIEMIdentityRiskSummary -Module Devolutions.CIEM -ErrorAction Stop | Should -Not -BeNullOrEmpty } It 'Has -PrincipalType parameter with ValidateSet' { $param = (Get-Command Get-CIEMIdentityRiskSummary).Parameters['PrincipalType'] $param | Should -Not -BeNullOrEmpty $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'User' $validateSet.ValidValues | Should -Contain 'ServicePrincipal' $validateSet.ValidValues | Should -Contain 'ManagedIdentity' $validateSet.ValidValues | Should -Contain 'Group' } It 'Has -RiskLevel parameter with ValidateSet' { $param = (Get-Command Get-CIEMIdentityRiskSummary).Parameters['RiskLevel'] $param | Should -Not -BeNullOrEmpty $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } $validateSet | Should -Not -BeNullOrEmpty $validateSet.ValidValues | Should -Contain 'Critical' $validateSet.ValidValues | Should -Contain 'High' $validateSet.ValidValues | Should -Contain 'Medium' $validateSet.ValidValues | Should -Contain 'Low' } It 'Has -SubscriptionId parameter' { $param = (Get-Command Get-CIEMIdentityRiskSummary).Parameters['SubscriptionId'] $param | Should -Not -BeNullOrEmpty } } Context 'No data' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $script:result = @(Get-CIEMIdentityRiskSummary) } It 'Returns empty array when no identity nodes exist' { $script:result | Should -HaveCount 0 } } Context 'Basic summary -- 2 users, 1 SP, 1 managed identity' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $now = Get-Date $collectedAt = $now.ToString('o') # User 1: Owner on subscription, last sign-in 120 days ago (Critical -- privileged + dormant) $signIn120DaysAgo = $now.AddDays(-120).ToString('o') Save-CIEMGraphNode -Id 'user-1' -Kind 'EntraUser' -DisplayName 'Alice Admin' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 120 lastSignIn = $signIn120DaysAgo lastInteractiveSignIn = $signIn120DaysAgo lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) # Target node for the role edge (subscription scope) Save-CIEMGraphNode -Id '/subscriptions/sub-1' -Kind 'AzureSubscription' -DisplayName 'Sub 1' ` -CollectedAt $collectedAt Save-CIEMGraphEdge -SourceId 'user-1' -TargetId '/subscriptions/sub-1' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Owner' privileged = $true scope = '/subscriptions/sub-1' definition_id = 'role-owner' } | ConvertTo-Json -Compress) # User 2: Reader only, signed in yesterday (Low -- read-only) $signInYesterday = $now.AddDays(-1).ToString('o') Save-CIEMGraphNode -Id 'user-2' -Kind 'EntraUser' -DisplayName 'Bob Reader' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 1 lastSignIn = $signInYesterday lastInteractiveSignIn = $signInYesterday lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) Save-CIEMGraphEdge -SourceId 'user-2' -TargetId '/subscriptions/sub-1' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Reader' privileged = $false scope = '/subscriptions/sub-1' definition_id = 'role-reader' } | ConvertTo-Json -Compress) # SP: Contributor on subscription, non-interactive sign-in 2 days ago (High -- privileged at sub scope) $spNonInteractive = $now.AddDays(-2).ToString('o') Save-CIEMGraphNode -Id 'sp-1' -Kind 'EntraServicePrincipal' -DisplayName 'Deploy SP' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 2 lastSignIn = $spNonInteractive lastInteractiveSignIn = $null lastNonInteractiveSignIn = $spNonInteractive servicePrincipalType = 'Application' } | ConvertTo-Json -Compress) Save-CIEMGraphEdge -SourceId 'sp-1' -TargetId '/subscriptions/sub-1' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Contributor' privileged = $true scope = '/subscriptions/sub-1' definition_id = 'role-contributor' } | ConvertTo-Json -Compress) # Managed identity: Owner on subscription, no sign-in data (Critical) Save-CIEMGraphNode -Id 'mi-1' -Kind 'EntraManagedIdentity' -DisplayName 'VM Managed Identity' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = $null lastSignIn = $null lastInteractiveSignIn = $null lastNonInteractiveSignIn = $null servicePrincipalType = 'ManagedIdentity' } | ConvertTo-Json -Compress) Save-CIEMGraphEdge -SourceId 'mi-1' -TargetId '/subscriptions/sub-1' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Owner' privileged = $true scope = '/subscriptions/sub-1' definition_id = 'role-owner' } | ConvertTo-Json -Compress) $script:result = @(Get-CIEMIdentityRiskSummary) } It 'Returns 4 identity rows' { $script:result | Should -HaveCount 4 } It 'Each row has expected properties including sign-in breakdown' { foreach ($row in $script:result) { $row.PSObject.Properties.Name | Should -Contain 'Id' $row.PSObject.Properties.Name | Should -Contain 'DisplayName' $row.PSObject.Properties.Name | Should -Contain 'PrincipalType' $row.PSObject.Properties.Name | Should -Contain 'AccountEnabled' $row.PSObject.Properties.Name | Should -Contain 'EntitlementCount' $row.PSObject.Properties.Name | Should -Contain 'PrivilegedCount' $row.PSObject.Properties.Name | Should -Contain 'InheritedCount' $row.PSObject.Properties.Name | Should -Contain 'LastSignIn' $row.PSObject.Properties.Name | Should -Contain 'DaysSinceSignIn' $row.PSObject.Properties.Name | Should -Contain 'RiskLevel' $row.PSObject.Properties.Name | Should -Contain 'LastInteractiveSignIn' $row.PSObject.Properties.Name | Should -Contain 'LastNonInteractiveSignIn' } } It 'EntitlementCount matches seeded role edges per identity' { ($script:result | Where-Object { $_.Id -eq 'user-1' }).EntitlementCount | Should -Be 1 ($script:result | Where-Object { $_.Id -eq 'user-2' }).EntitlementCount | Should -Be 1 ($script:result | Where-Object { $_.Id -eq 'sp-1' }).EntitlementCount | Should -Be 1 ($script:result | Where-Object { $_.Id -eq 'mi-1' }).EntitlementCount | Should -Be 1 } It 'PrivilegedCount correctly counts privileged role edges' { ($script:result | Where-Object { $_.Id -eq 'user-1' }).PrivilegedCount | Should -Be 1 ($script:result | Where-Object { $_.Id -eq 'user-2' }).PrivilegedCount | Should -Be 0 ($script:result | Where-Object { $_.Id -eq 'sp-1' }).PrivilegedCount | Should -Be 1 ($script:result | Where-Object { $_.Id -eq 'mi-1' }).PrivilegedCount | Should -Be 1 } It 'User with Owner on subscription and no sign-in in 90d gets Critical risk' { ($script:result | Where-Object { $_.Id -eq 'user-1' }).RiskLevel | Should -Be 'Critical' } It 'User with Reader only gets Low risk' { ($script:result | Where-Object { $_.Id -eq 'user-2' }).RiskLevel | Should -Be 'Low' } It 'SP with Contributor on subscription and recent non-interactive sign-in gets High risk' { ($script:result | Where-Object { $_.Id -eq 'sp-1' }).RiskLevel | Should -Be 'High' } It 'SP LastSignIn uses non-interactive date when no interactive exists' { $sp = $script:result | Where-Object { $_.Id -eq 'sp-1' } $sp.LastSignIn | Should -Not -BeNullOrEmpty $sp.DaysSinceSignIn | Should -BeLessOrEqual 5 $sp.LastInteractiveSignIn | Should -BeNullOrEmpty $sp.LastNonInteractiveSignIn | Should -Not -BeNullOrEmpty } It 'Managed identity with Owner and no sign-in data gets Critical risk' { ($script:result | Where-Object { $_.Id -eq 'mi-1' }).RiskLevel | Should -Be 'Critical' } It 'DaysSinceSignIn is null when no sign-in data exists' { ($script:result | Where-Object { $_.Id -eq 'mi-1' }).DaysSinceSignIn | Should -BeNullOrEmpty } It 'Reports correct PrincipalType for managed identity' { ($script:result | Where-Object { $_.Id -eq 'mi-1' }).PrincipalType | Should -Be 'ManagedIdentity' } It 'Reports correct PrincipalType for regular SP' { ($script:result | Where-Object { $_.Id -eq 'sp-1' }).PrincipalType | Should -Be 'ServicePrincipal' } } Context '-PrincipalType filter' { # Data seeded in previous context is still present BeforeAll { $script:usersOnly = @(Get-CIEMIdentityRiskSummary -PrincipalType User) $script:spsOnly = @(Get-CIEMIdentityRiskSummary -PrincipalType ServicePrincipal) $script:misOnly = @(Get-CIEMIdentityRiskSummary -PrincipalType ManagedIdentity) } It 'Returns only User identities when -PrincipalType User' { $script:usersOnly | Should -HaveCount 2 $script:usersOnly | ForEach-Object { $_.PrincipalType | Should -Be 'User' } } It 'Returns only ServicePrincipal (non-MI) when -PrincipalType ServicePrincipal' { $script:spsOnly | Should -HaveCount 1 $script:spsOnly[0].DisplayName | Should -Be 'Deploy SP' } It 'Returns only ManagedIdentity when -PrincipalType ManagedIdentity' { $script:misOnly | Should -HaveCount 1 $script:misOnly[0].DisplayName | Should -Be 'VM Managed Identity' } } Context '-RiskLevel filter' { It 'Returns only Critical identities when -RiskLevel Critical' { $criticals = @(Get-CIEMIdentityRiskSummary -RiskLevel Critical) $criticals.Count | Should -BeGreaterOrEqual 1 $criticals | ForEach-Object { $_.RiskLevel | Should -Be 'Critical' } } It 'Returns only Low identities when -RiskLevel Low' { $lows = @(Get-CIEMIdentityRiskSummary -RiskLevel Low) $lows.Count | Should -BeGreaterOrEqual 1 $lows | ForEach-Object { $_.RiskLevel | Should -Be 'Low' } } } Context 'Risk computation edge cases' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $collectedAt = (Get-Date).ToString('o') # Target nodes for edges Save-CIEMGraphNode -Id '/subscriptions/sub-1' -Kind 'AzureSubscription' -DisplayName 'Sub 1' -CollectedAt $collectedAt 1..6 | ForEach-Object { Save-CIEMGraphNode -Id "/subscriptions/sub-1/resourceGroups/rg-$_" -Kind 'AzureResourceGroup' ` -DisplayName "rg-$_" -CollectedAt $collectedAt } # Disabled user with privileged role = Critical Save-CIEMGraphNode -Id 'user-disabled' -Kind 'EntraUser' -DisplayName 'Disabled Admin' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $false daysSinceSignIn = 10 lastSignIn = (Get-Date).AddDays(-10).ToString('o') lastInteractiveSignIn = (Get-Date).AddDays(-10).ToString('o') lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) Save-CIEMGraphEdge -SourceId 'user-disabled' -TargetId '/subscriptions/sub-1' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Owner' privileged = $true scope = '/subscriptions/sub-1' definition_id = 'role-owner' } | ConvertTo-Json -Compress) # User with 6 non-privileged roles = Medium (>5 entitlements) Save-CIEMGraphNode -Id 'user-many-roles' -Kind 'EntraUser' -DisplayName 'Many Roles User' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 1 lastSignIn = (Get-Date).AddDays(-1).ToString('o') lastInteractiveSignIn = (Get-Date).AddDays(-1).ToString('o') lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) 1..6 | ForEach-Object { Save-CIEMGraphEdge -SourceId 'user-many-roles' ` -TargetId "/subscriptions/sub-1/resourceGroups/rg-$_" ` -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = "Custom Role $_" privileged = $false scope = "/subscriptions/sub-1/resourceGroups/rg-$_" definition_id = "role-custom-$_" } | ConvertTo-Json -Compress) } # User with group-inherited Owner on subscription = High (InheritedRole edge) Save-CIEMGraphNode -Id 'user-inherited' -Kind 'EntraUser' -DisplayName 'Inherited User' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 5 lastSignIn = (Get-Date).AddDays(-5).ToString('o') lastInteractiveSignIn = (Get-Date).AddDays(-5).ToString('o') lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) Save-CIEMGraphEdge -SourceId 'user-inherited' -TargetId '/subscriptions/sub-1' -Kind 'InheritedRole' ` -Computed 1 -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Owner' privileged = $true scope = '/subscriptions/sub-1' definition_id = 'role-owner' } | ConvertTo-Json -Compress) $script:edgeCases = @(Get-CIEMIdentityRiskSummary) } It 'Disabled user with privileged roles gets Critical' { ($script:edgeCases | Where-Object { $_.Id -eq 'user-disabled' }).RiskLevel | Should -Be 'Critical' } It 'User with >5 assignments but no privileged roles gets Medium' { ($script:edgeCases | Where-Object { $_.Id -eq 'user-many-roles' }).RiskLevel | Should -Be 'Medium' } It 'User with group-inherited Owner gets High' { $inherited = $script:edgeCases | Where-Object { $_.Id -eq 'user-inherited' } $inherited.RiskLevel | Should -Be 'High' $inherited.InheritedCount | Should -Be 1 } } Context '-SubscriptionId filter' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $collectedAt = (Get-Date).ToString('o') Save-CIEMGraphNode -Id '/subscriptions/sub-1' -Kind 'AzureSubscription' -DisplayName 'Sub 1' -CollectedAt $collectedAt Save-CIEMGraphNode -Id '/subscriptions/sub-2' -Kind 'AzureSubscription' -DisplayName 'Sub 2' -CollectedAt $collectedAt Save-CIEMGraphNode -Id 'user-sub-filter' -Kind 'EntraUser' -DisplayName 'Sub Filter User' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 5 lastSignIn = (Get-Date).AddDays(-5).ToString('o') lastInteractiveSignIn = (Get-Date).AddDays(-5).ToString('o') lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) # Role on sub-1 Save-CIEMGraphEdge -SourceId 'user-sub-filter' -TargetId '/subscriptions/sub-1' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Reader' privileged = $false scope = '/subscriptions/sub-1' definition_id = 'role-reader' } | ConvertTo-Json -Compress) # Role on sub-2 Save-CIEMGraphEdge -SourceId 'user-sub-filter' -TargetId '/subscriptions/sub-2' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Contributor' privileged = $true scope = '/subscriptions/sub-2' definition_id = 'role-contrib' } | ConvertTo-Json -Compress) } It 'Returns identity with assignments scoped to the specified subscription' { $filtered = @(Get-CIEMIdentityRiskSummary -SubscriptionId 'sub-1') $filtered | Should -HaveCount 1 $filtered[0].EntitlementCount | Should -Be 1 } It 'Returns identity with zero entitlements when subscription has no matching edges' { $filtered = @(Get-CIEMIdentityRiskSummary -SubscriptionId 'sub-nonexistent') $filtered | Should -HaveCount 1 $filtered[0].EntitlementCount | Should -Be 0 } } Context 'DaysSinceSignIn from node properties' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $collectedAt = '2026-03-01T12:00:00' Save-CIEMGraphNode -Id '/subscriptions/sub-1' -Kind 'AzureSubscription' -DisplayName 'Sub 1' ` -CollectedAt $collectedAt # Seed with explicit daysSinceSignIn = 90 (pre-computed during graph build) Save-CIEMGraphNode -Id 'user-ref-date' -Kind 'EntraUser' -DisplayName 'RefDate User' ` -CollectedAt $collectedAt -Properties (@{ accountEnabled = $true daysSinceSignIn = 90 lastSignIn = '2025-12-01T12:00:00' lastInteractiveSignIn = '2025-12-01T12:00:00' lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) Save-CIEMGraphEdge -SourceId 'user-ref-date' -TargetId '/subscriptions/sub-1' -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = 'Reader' privileged = $false scope = '/subscriptions/sub-1' definition_id = 'role-reader' } | ConvertTo-Json -Compress) $script:refDateResult = @(Get-CIEMIdentityRiskSummary) } It 'Reads DaysSinceSignIn directly from node properties JSON' { $user = $script:refDateResult | Where-Object { $_.Id -eq 'user-ref-date' } $user.DaysSinceSignIn | Should -Be 90 } } Context 'Identity node with no role edges' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $collectedAt = (Get-Date).ToString('o') Save-CIEMGraphNode -Id 'user-no-roles' -Kind 'EntraUser' -DisplayName 'No Roles User' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 5 lastSignIn = (Get-Date).AddDays(-5).ToString('o') lastInteractiveSignIn = (Get-Date).AddDays(-5).ToString('o') lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) $script:noRolesResult = @(Get-CIEMIdentityRiskSummary) } It 'Returns identity with zero entitlements and Low risk' { $script:noRolesResult | Should -HaveCount 1 $script:noRolesResult[0].EntitlementCount | Should -Be 0 $script:noRolesResult[0].PrivilegedCount | Should -Be 0 $script:noRolesResult[0].InheritedCount | Should -Be 0 $script:noRolesResult[0].RiskLevel | Should -Be 'Low' } } Context 'Entitlement threshold boundary at exactly 5 assignments' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $collectedAt = (Get-Date).ToString('o') Save-CIEMGraphNode -Id '/subscriptions/sub-1' -Kind 'AzureSubscription' -DisplayName 'Sub 1' -CollectedAt $collectedAt 1..5 | ForEach-Object { Save-CIEMGraphNode -Id "/subscriptions/sub-1/resourceGroups/rg-boundary-$_" -Kind 'AzureResourceGroup' ` -DisplayName "rg-boundary-$_" -CollectedAt $collectedAt } # Active user with exactly 5 non-privileged role assignments Save-CIEMGraphNode -Id 'user-boundary-5' -Kind 'EntraUser' -DisplayName 'Boundary 5 User' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 1 lastSignIn = (Get-Date).AddDays(-1).ToString('o') lastInteractiveSignIn = (Get-Date).AddDays(-1).ToString('o') lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) 1..5 | ForEach-Object { Save-CIEMGraphEdge -SourceId 'user-boundary-5' ` -TargetId "/subscriptions/sub-1/resourceGroups/rg-boundary-$_" ` -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = "Custom Role $_" privileged = $false scope = "/subscriptions/sub-1/resourceGroups/rg-boundary-$_" definition_id = "role-custom-$_" } | ConvertTo-Json -Compress) } $script:boundary5Result = @(Get-CIEMIdentityRiskSummary) } It 'Identity with exactly 5 non-privileged assignments gets Low risk (threshold uses -gt not -ge)' { $user = $script:boundary5Result | Where-Object { $_.Id -eq 'user-boundary-5' } $user.EntitlementCount | Should -Be 5 $user.RiskLevel | Should -Be 'Low' } } Context 'Entitlement threshold boundary at 6 assignments' { BeforeAll { Invoke-CIEMQuery -Query "DELETE FROM graph_edges" Invoke-CIEMQuery -Query "DELETE FROM graph_nodes" $collectedAt = (Get-Date).ToString('o') Save-CIEMGraphNode -Id '/subscriptions/sub-1' -Kind 'AzureSubscription' -DisplayName 'Sub 1' -CollectedAt $collectedAt 1..6 | ForEach-Object { Save-CIEMGraphNode -Id "/subscriptions/sub-1/resourceGroups/rg-boundary-$_" -Kind 'AzureResourceGroup' ` -DisplayName "rg-boundary-$_" -CollectedAt $collectedAt } # Active user with 6 non-privileged role assignments (one past threshold) Save-CIEMGraphNode -Id 'user-boundary-6' -Kind 'EntraUser' -DisplayName 'Boundary 6 User' ` -CollectedAt $collectedAt ` -Properties (@{ accountEnabled = $true daysSinceSignIn = 1 lastSignIn = (Get-Date).AddDays(-1).ToString('o') lastInteractiveSignIn = (Get-Date).AddDays(-1).ToString('o') lastNonInteractiveSignIn = $null } | ConvertTo-Json -Compress) 1..6 | ForEach-Object { Save-CIEMGraphEdge -SourceId 'user-boundary-6' ` -TargetId "/subscriptions/sub-1/resourceGroups/rg-boundary-$_" ` -Kind 'HasRole' ` -CollectedAt $collectedAt ` -Properties (@{ role_name = "Custom Role $_" privileged = $false scope = "/subscriptions/sub-1/resourceGroups/rg-boundary-$_" definition_id = "role-custom-$_" } | ConvertTo-Json -Compress) } $script:boundary6Result = @(Get-CIEMIdentityRiskSummary) } It 'Identity with 6 non-privileged assignments gets Medium risk (one past threshold)' { $user = $script:boundary6Result | Where-Object { $_.Id -eq 'user-boundary-6' } $user.EntitlementCount | Should -Be 6 $user.RiskLevel | Should -Be 'Medium' } } } |