tests/Get-MsecSentinelIncident.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecSentinelIncident. It reads the Sentinel incidents ARM API on the Az # context, because Sentinel is an Azure resource and the msec app holds Graph permissions. # # The traps: # # PROVIDERNAME IS WHY IT EXISTS. 'Azure Sentinel' incidents never reach Defender XDR, so # Get-MsecDefenderIncident cannot see them - measured, 628 of 3,721 on one workspace. # # NOT PAGING IS A SILENT TRUNCATION. The API caps a page well below the total. # # "COULD NOT READ" MUST NOT LOOK LIKE "NOTHING THERE", for a workspace or for entities. BeforeAll { Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop } AfterAll { Remove-Module Msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecSentinelIncident' { BeforeEach { InModuleScope Msec { Mock Resolve-MsecSentinelWorkspace -MockWith { [pscustomobject]@{ Headers = @{ Authorization = 'Bearer t' } Subscription = '00000000-0000-0000-0000-000000000000' Workspaces = @([pscustomobject]@{ Name = 'contoso-sentinel'; ResourceGroupName = 'rg' ResourceId = '/subscriptions/x/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/contoso-sentinel' Base = 'https://management.azure.com/subscriptions/x/.../providers/Microsoft.SecurityInsights' }) } } } } It 'follows nextLink, because one page is a silently short answer' { $r = InModuleScope Msec { $script:Calls = 0 Mock Invoke-RestMethod -MockWith { $script:Calls++ if ($script:Calls -eq 1) { @{ value = @(@{ name = 'g1'; etag = 'e1'; properties = @{ incidentNumber = 1; title = 'a'; status = 'New'; severity = 'High'; providerName = 'Azure Sentinel'; additionalData = @{ alertsCount = 1 } } }) nextLink = 'https://management.azure.com/page2' } } else { @{ value = @(@{ name = 'g2'; etag = 'e2'; properties = @{ incidentNumber = 2; title = 'b'; status = 'New'; severity = 'Low'; providerName = 'Microsoft XDR'; additionalData = @{ alertsCount = 2 } } }) } } } Get-MsecSentinelIncident -WorkspaceName contoso-sentinel } @($r).Count | Should -Be 2 @($r).Number | Should -Contain 2 } It 'surfaces ProviderName, which is the whole point of reading ARM instead of XDR' { $r = InModuleScope Msec { Mock Invoke-RestMethod -MockWith { @{ value = @(@{ name = 'g1'; etag = 'e1'; properties = @{ incidentNumber = 51; title = 'old'; status = 'New'; severity = 'High'; providerName = 'Azure Sentinel'; additionalData = @{ alertsCount = 1 } } }) } } Get-MsecSentinelIncident -WorkspaceName contoso-sentinel } $r.ProviderName | Should -Be 'Azure Sentinel' $r.Etag | Should -Be 'e1' $r.Name | Should -Be 'g1' } It 'names Microsoft Sentinel Reader on a 403, and says it is not an Entra grant' { # Sending someone to check the app registration wastes the afternoon: this is Azure # RBAC on the workspace and the msec app cannot be given it. $msg = InModuleScope Msec { Mock Invoke-RestMethod -MockWith { throw 'Response status code does not indicate success: 403 (Forbidden).' } try { Get-MsecSentinelIncident -WorkspaceName contoso-sentinel | Out-Null } catch { $_.Exception.Message } } $msg | Should -BeLike '*Microsoft Sentinel Reader*' $msg | Should -BeLike '*NOT*granted through Entra*' } It 'skips a workspace with no Sentinel endpoint instead of failing the whole read' { # An ordinary log workspace 404s here. One of those must not stop a subscription read. $r = InModuleScope Msec { Mock Invoke-RestMethod -MockWith { throw 'Response status code does not indicate success: 404 (NotFound).' } Get-MsecSentinelIncident -WorkspaceName contoso-sentinel -WarningAction SilentlyContinue } @($r).Count | Should -Be 0 } It 'leaves Entities $null when they could not be read, never an empty list' { # An empty array would read as "this incident has no entities", which is a different # and wrong answer. $r = InModuleScope Msec { Mock Invoke-RestMethod -ParameterFilter { $Method -eq 'Post' } -MockWith { throw 'boom' } Mock Invoke-RestMethod -MockWith { @{ value = @(@{ name = 'g1'; etag = 'e1'; properties = @{ incidentNumber = 1; title = 'a'; status = 'New'; severity = 'High'; providerName = 'Azure Sentinel'; additionalData = @{ alertsCount = 1 } } }) } } Get-MsecSentinelIncident -WorkspaceName contoso-sentinel -IncludeEntities -WarningAction SilentlyContinue } $r.Entities | Should -BeNullOrEmpty $r.Number | Should -Be 1 } It 'refuses -Days together with -AnyAge rather than silently preferring one' { InModuleScope Msec { { Get-MsecSentinelIncident -WorkspaceName contoso-sentinel -Days 7 -AnyAge } | Should -Throw '*contradict*' } } } |