tests/Get-MsecAppGatewayClientActivity.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecAppGatewayClientActivity. # # The command exists to stop two specific wrong answers: # - treating a 200 on a login page as a sign-in, which makes every crawler look like an # intruder # - reporting Authenticated = $false for a period where the only data is the hourly summary, # which has no HTTP method and therefore cannot answer the question at all # Most of these cover those, plus refusing a malformed address before it becomes a quiet # empty result. BeforeAll { Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop } AfterAll { Remove-Module Msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecAppGatewayClientActivity' { BeforeEach { InModuleScope Msec { Mock Search-MsecAzureResourceGraph -MockWith { @([pscustomobject]@{ Name='prod-sentinel-log'; ResourceGroupName='siem' SubscriptionId='sub1'; CustomerId='11111111-1111-1111-1111-111111111111' }) } } } It 'refuses a malformed address before querying, rather than returning an empty result' { $err = InModuleScope Msec { Mock Invoke-AzOperationalInsightsQuery -MockWith { throw 'must not be called' } try { Get-MsecAppGatewayClientActivity -ClientIp '79.137.138.24/32' -WorkspaceName 'prod-sentinel-log' $null } catch { "$($_.Exception.Message)" } } $err | Should -Match 'not a valid IP address' # The reason this is an error and not a filter: a bad address matches nothing, and an # empty result reads as "this address did nothing". $err | Should -Match 'empty result rather than an error' InModuleScope Msec { Should -Invoke Invoke-AzOperationalInsightsQuery -Times 0 -Exactly } } It 'marks the OIDC completion points as authenticated and a login page as not' { $rows = InModuleScope Msec { Mock Invoke-AzOperationalInsightsQuery -MockWith { [pscustomobject]@{ Results = @( [pscustomobject]@{ TimeGenerated='2026-09-27T19:00:00Z'; ClientIp='1.2.3.4'; Method='GET' Uri='/Account/Login'; Status=200; Host='idp'; Agent='Chrome'; Sent=100 Requests=1; Grain='per-request'; Authenticated=$false } [pscustomobject]@{ TimeGenerated='2026-09-27T19:05:00Z'; ClientIp='1.2.3.4'; Method='POST' Uri='/signin-oidc'; Status=302; Host='web'; Agent='Chrome'; Sent=200 Requests=1; Grain='per-request'; Authenticated=$true } ) } } , @(Get-MsecAppGatewayClientActivity -ClientIp '1.2.3.4' -WorkspaceName 'prod-sentinel-log') } $login = $rows | Where-Object Uri -eq '/Account/Login' $oidc = $rows | Where-Object Uri -eq '/signin-oidc' # A rendered login page is not a sign-in. Crawlers produce these constantly. $login.Authenticated | Should -BeFalse $oidc.Authenticated | Should -BeTrue } It 'reports Authenticated as null on summary rows, never false' { $row = InModuleScope Msec { Mock Invoke-AzOperationalInsightsQuery -MockWith { [pscustomobject]@{ Results = @( [pscustomobject]@{ TimeGenerated='2026-10-07T18:00:00Z'; ClientIp='1.2.3.4'; Method='' Uri='["/Account/Login"]'; Status=0; Host='["idp"]'; Agent='["Chrome"]' Sent=5000; Requests=10; Grain='hourly-summary'; Authenticated=$null } ) } } Get-MsecAppGatewayClientActivity -ClientIp '1.2.3.4' -WorkspaceName 'prod-sentinel-log' } $row.Grain | Should -Be 'hourly-summary' # The summary has no HTTP method, so the question cannot be answered - and $false would # assert something that was never measured. $row.Authenticated | Should -BeNullOrEmpty # A zero status is 'not recorded', not 'HTTP 0'. $row.Status | Should -BeNullOrEmpty } It 'in -SummaryOnly, distinguishes "did not authenticate" from "could not tell"' { $out = InModuleScope Msec { Mock Invoke-AzOperationalInsightsQuery -MockWith { [pscustomobject]@{ Results = @( # Per-request data exists and shows no authentication -> a real $false. [pscustomobject]@{ TimeGenerated='2026-10-01T10:00:00Z'; ClientIp='1.1.1.1'; Method='GET' Uri='/robots.txt'; Status=404; Host='idp'; Agent='bot'; Sent=10 Requests=1; Grain='per-request'; Authenticated=$false } # Only summary data -> unknown, must not be reported as $false. [pscustomobject]@{ TimeGenerated='2026-10-07T18:00:00Z'; ClientIp='2.2.2.2'; Method='' Uri='["/"]'; Status=0; Host='["web"]'; Agent='["x"]'; Sent=99 Requests=7; Grain='hourly-summary'; Authenticated=$null } ) } } , @(Get-MsecAppGatewayClientActivity -ClientIp '1.1.1.1','2.2.2.2' -WorkspaceName 'prod-sentinel-log' -SummaryOnly) } ($out | Where-Object ClientIp -eq '1.1.1.1').Authenticated | Should -BeFalse ($out | Where-Object ClientIp -eq '2.2.2.2').Authenticated | Should -BeNullOrEmpty ($out | Where-Object ClientIp -eq '2.2.2.2').Requests | Should -Be 7 } It 'warns on no activity, because a Basic-plan table also returns nothing' { $warning = InModuleScope Msec { Mock Invoke-AzOperationalInsightsQuery -MockWith { [pscustomobject]@{ Results = @() } } $w = @() Get-MsecAppGatewayClientActivity -ClientIp '1.2.3.4' -WorkspaceName 'prod-sentinel-log' ` -WarningVariable w -WarningAction SilentlyContinue | Out-Null "$($w -join ' ')" } $warning | Should -Match 'NOT proof the address was never seen' $warning | Should -Match 'Basic-plan' } It 'lists the candidates when the workspace name is unknown' { $err = InModuleScope Msec { Mock Invoke-AzOperationalInsightsQuery -MockWith { throw 'must not be called' } try { Get-MsecAppGatewayClientActivity -ClientIp '1.2.3.4' -WorkspaceName 'nope'; $null } catch { "$($_.Exception.Message)" } } $err | Should -Match 'prod-sentinel-log' } } |