tests/Connect-MsecAdmin.Tests.ps1
|
#Requires -Module Pester # # Tests for Connect-MsecAdmin. # # This command exists because the app CANNOT write - every permission New-MsecApp consents is # *.Read.All - so writes run as a person instead. Two things it must get right, both of which # fail silently otherwise: # # A declined scope is not an error. Connect-MgGraph returns a context without it and the # first write 403s naming nothing, so the granted set is checked against the requested one # here, while there is still something useful to say. # # Signing in to a different tenant than Connect-Msec is reading is invisible at the time and # obvious afterwards. It is refused, and the half-open Graph session is closed on the way out. BeforeAll { $script:StubbedGraphCommands = @() Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop # Microsoft.Graph.Authentication is NOT a dependency of msec - only Connect-MsecAdmin needs # it, and it checks at run time. It is preinstalled on the Windows and Ubuntu GitHub images # but NOT on the macOS one, so these tests passed on two runners and failed on the third: # Pester's Mock requires the command to EXIST, and a missing one fails as "Could not find # Command Get-MgContext" rather than as anything pointing at the real cause. # # Stubbed when absent OR unusable, so a machine with a working module still mocks the # real command and the two behave identically. # The stubs carry the REAL parameter names, not an empty param(). A -ParameterFilter is # evaluated against the mocked command's own signature, so a stub without -TenantId leaves # $TenantId unbound and the filter silently matches nothing - the mock is invoked, the # assertion counts zero calls, and the failure looks like the command was never called. $graphStubs = @{ 'Get-MgContext' = { [CmdletBinding()] param() } 'Disconnect-MgGraph' = { [CmdletBinding()] param() } 'Connect-MgGraph' = { [CmdletBinding()] param( [string[]] $Scopes, [string] $TenantId, [switch] $NoWelcome, $AccessToken, [string] $Environment ) } } foreach ($graphCmd in $graphStubs.Keys) { # DISCOVERABLE IS NOT USABLE, and testing only for presence is what broke CI on the # ubuntu runner: Get-MgContext resolved as a command while its backing assembly # (Microsoft.Graph.Authentication.Core) could not be loaded, so the stub was skipped # and Pester's Mock then died building CommandMetadata - # "Cannot convert value Get-MgContext to type CommandMetadata" # which points at the mock rather than at the half-installed module behind it. So the # probe is the same one Mock performs: build the metadata, and stub when that fails. # A global function shadows a cmdlet of the same name, so the stub wins either way. $usable = $false $found = Get-Command $graphCmd -ErrorAction SilentlyContinue if ($found) { try { $null = [System.Management.Automation.CommandMetadata]::new($found); $usable = $true } catch { $usable = $false } } if (-not $usable) { Set-Item "function:global:$graphCmd" -Value $graphStubs[$graphCmd] -Force $script:StubbedGraphCommands += $graphCmd } } } AfterAll { foreach ($graphCmd in $script:StubbedGraphCommands) { Remove-Item "function:global:$graphCmd" -ErrorAction SilentlyContinue } Remove-Module msec -Force -ErrorAction SilentlyContinue } Describe 'Connect-MsecAdmin' { It 'refuses when the tenant granted fewer scopes than were asked for' { InModuleScope msec { Mock Get-Module -ParameterFilter { $ListAvailable } -MockWith { [pscustomobject]@{ Name = 'Microsoft.Graph.Authentication' } } Mock Import-Module -MockWith { } Mock Connect-MgGraph -MockWith { } # Asked for two, granted one - which Connect-MgGraph reports as success. Mock Get-MgContext -MockWith { [pscustomobject]@{ Account = 'me@contoso.com'; TenantId = 't1' Scopes = @('SecurityAlert.ReadWrite.All') } } $script:MsecSession = $null { Connect-MsecAdmin -Scope 'SecurityAlert.ReadWrite.All', 'SecurityIncident.ReadWrite.All' } | Should -Throw '*SecurityIncident.ReadWrite.All*' } } It 'refuses a tenant different from the one being read, and closes the session' { InModuleScope msec { Mock Get-Module -ParameterFilter { $ListAvailable } -MockWith { [pscustomobject]@{ Name = 'Microsoft.Graph.Authentication' } } Mock Import-Module -MockWith { } Mock Connect-MgGraph -MockWith { } Mock Disconnect-MgGraph -MockWith { } Mock Get-MgContext -MockWith { [pscustomobject]@{ Account = 'me@contoso.com'; TenantId = 'OTHER-TENANT' Scopes = @('SecurityAlert.ReadWrite.All') } } # Reading tenant-1; the sign-in landed somewhere else. $script:MsecSession = @{ TenantId = 'tenant-1' } { Connect-MsecAdmin -Scope 'SecurityAlert.ReadWrite.All' -TenantId 'OTHER-TENANT' } | Should -Throw '*different tenant*' # The half-open session must not be left behind for a later command to find. Should -Invoke Disconnect-MgGraph -Times 1 -Exactly } } It 'defaults to the tenant Connect-Msec is reading' { InModuleScope msec { Mock Get-Module -ParameterFilter { $ListAvailable } -MockWith { [pscustomobject]@{ Name = 'Microsoft.Graph.Authentication' } } Mock Import-Module -MockWith { } Mock Connect-MgGraph -MockWith { } Mock Get-MgContext -MockWith { [pscustomobject]@{ Account = 'me@contoso.com'; TenantId = 'tenant-1' Scopes = @('SecurityAlert.ReadWrite.All') } } $script:MsecSession = @{ TenantId = 'tenant-1' } $null = Connect-MsecAdmin -Scope 'SecurityAlert.ReadWrite.All' Should -Invoke Connect-MgGraph -Times 1 -Exactly -ParameterFilter { $TenantId -eq 'tenant-1' } } } It 'records the write session separately from the read session' { $session = InModuleScope msec { Mock Get-Module -ParameterFilter { $ListAvailable } -MockWith { [pscustomobject]@{ Name = 'Microsoft.Graph.Authentication' } } Mock Import-Module -MockWith { } Mock Connect-MgGraph -MockWith { } Mock Get-MgContext -MockWith { [pscustomobject]@{ Account = 'me@contoso.com'; TenantId = 'tenant-1' Scopes = @('SecurityAlert.ReadWrite.All') } } $script:MsecSession = @{ TenantId = 'tenant-1' } $null = Connect-MsecAdmin -Scope 'SecurityAlert.ReadWrite.All' # A Set-* has to tell "no write session" from "app session" - so they are distinct. $script:MsecAdminSession } $session.Account | Should -Be 'me@contoso.com' $session.GrantedScope | Should -Contain 'SecurityAlert.ReadWrite.All' } It 'names the module to install when the Graph SDK is absent' { InModuleScope msec { Mock Get-Module -ParameterFilter { $ListAvailable } -MockWith { $null } { Connect-MsecAdmin } | Should -Throw '*Microsoft.Graph.Authentication*' } } } |