tests/Get-MsecAzureDomainService.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecAzureDomainService.
#
# The settings themselves come straight out of Resource Graph and are the .kql file's business.
# What is specific to this command is the audit-log half, and the distinction it exists to keep:
#
# AuditLogsEnabled $false means "security audit is off on this managed domain" - a real
# finding, and the default state. $null means the diagnostic settings could not be READ, which
# is a permission problem and not a finding at all. A command that collapsed the two would
# report every domain it was refused as a domain nobody is auditing.
#
# The other trap is the category group. A diagnostic setting that selects a GROUP ('allLogs')
# leaves every per-category field null, so reading only `category` reports the most complete
# possible logging configuration as no logging at all.

BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..' 'msec.psm1'
    Import-Module $modulePath -Force -ErrorAction Stop
}

AfterAll {
    Remove-Module msec -Force -ErrorAction SilentlyContinue
}

Describe 'Get-MsecAzureDomainService' {

    BeforeEach {
        InModuleScope msec {
            Mock Get-AzContext -MockWith { [pscustomobject]@{ Name = 'ctx' } }
            Mock Search-MsecAzureResourceGraph -MockWith {
                [pscustomobject]@{
                    Domain = 'contoso.local'
                    NtlmV1 = 'Enabled'
                    WeakSettings = 'NTLM v1 accepted'
                    Id = '/subscriptions/s1/resourceGroups/rg/providers/Microsoft.AAD/DomainServices/contoso.local'
                }
            }
            function script:Set-DiagnosticResponse {
                param([int] $Status = 200, [string] $Body, [switch] $Throw)
                Mock Invoke-AzRestMethod -MockWith {
                    if ($Throw) { throw 'AuthorizationFailed' }
                    [pscustomobject]@{ StatusCode = $Status; Content = $Body }
                }.GetNewClosure()
            }
        }
    }

    It 'reports a domain with no diagnostic setting as unaudited, not unknown' {
        $row = InModuleScope msec {
            . Set-DiagnosticResponse -Body '{"value":[]}'
            Get-MsecAzureDomainService
        }

        # The default state of a managed domain, and a real finding: every authentication
        # against it, including every failure, goes unrecorded.
        $row.AuditLogsEnabled  | Should -BeOfType [bool]
        $row.AuditLogsEnabled  | Should -BeFalse
        $row.AuditLogWorkspace | Should -BeNullOrEmpty
    }

    It 'reports a refused read as unknown, not as unaudited' {
        $row = InModuleScope msec {
            . Set-DiagnosticResponse -Throw
            Get-MsecAzureDomainService -WarningAction SilentlyContinue
        }

        # THE distinction this command turns on: $null is "could not measure".
        $row.AuditLogsEnabled | Should -BeNullOrEmpty
        $row.AuditLogsEnabled | Should -Not -BeOfType [bool]
    }

    It 'names the category group when no individual categories are selected' {
        $row = InModuleScope msec {
            # What a real managed domain returns: category null, categoryGroup set.
            . Set-DiagnosticResponse -Body (@'
{"value":[{"name":"aadds-audit","properties":{
  "workspaceId":"/subscriptions/s1/resourcegroups/rg/providers/microsoft.operationalinsights/workspaces/security-law",
  "logs":[{"category":null,"categoryGroup":"audit","enabled":true},
          {"category":null,"categoryGroup":"allLogs","enabled":true}]}}]}
'@
)
            Get-MsecAzureDomainService
        }

        $row.AuditLogsEnabled   | Should -BeTrue
        # 'allLogs' and 'no categories' are opposite answers.
        $row.AuditLogCategories | Should -Match 'allLogs'
        $row.AuditLogCategories | Should -Match 'audit'
    }

    It 'hands back the workspace NAME, which is what Search-MsecLogAnalytics takes' {
        $row = InModuleScope msec {
            . Set-DiagnosticResponse -Body (@'
{"value":[{"name":"aadds-audit","properties":{
  "workspaceId":"/subscriptions/s1/resourcegroups/rg/providers/microsoft.operationalinsights/workspaces/security-law",
  "logs":[{"category":"AADDomainServicesAccountLogon","enabled":true}]}}]}
'@
)
            Get-MsecAzureDomainService
        }

        # The whole point of the column: a tenant has dozens of workspaces and the one a managed
        # domain writes to is not guessable.
        $row.AuditLogWorkspace   | Should -Be 'security-law'
        $row.AuditLogWorkspaceId | Should -BeLike '/subscriptions/s1/*'
        $row.AuditLogCategories  | Should -Be 'AADDomainServicesAccountLogon'
    }

    It 'treats a setting whose logs are all switched off as no logging' {
        $row = InModuleScope msec {
            . Set-DiagnosticResponse -Body (@'
{"value":[{"name":"metrics-only","properties":{
  "workspaceId":"/subscriptions/s1/resourcegroups/rg/providers/microsoft.operationalinsights/workspaces/security-law",
  "logs":[{"category":null,"categoryGroup":"audit","enabled":false}],
  "metrics":[{"category":"AllMetrics","enabled":true}]}}]}
'@
)
            Get-MsecAzureDomainService
        }

        # A diagnostic setting exists, so a check for one would pass - and nothing is being
        # logged. Matched on the enabled log entries rather than on the setting existing.
        $row.AuditLogsEnabled  | Should -BeFalse
        $row.AuditLogWorkspace | Should -BeNullOrEmpty
    }

    It 'carries every Resource Graph column through rather than a hand-picked subset' {
        $row = InModuleScope msec {
            . Set-DiagnosticResponse -Body '{"value":[]}'
            Get-MsecAzureDomainService
        }

        # A setting this command was never taught about is still worth seeing.
        $row.NtlmV1       | Should -Be 'Enabled'
        $row.WeakSettings | Should -Be 'NTLM v1 accepted'
    }

    It 'throws a clear error when there is no Azure context' {
        InModuleScope msec {
            Mock Get-AzContext -MockWith { $null }
            { Get-MsecAzureDomainService } | Should -Throw '*Connect-AzAccount*'
        }
    }
}