tests/Test-Assessment.41211.ps1

<#
.SYNOPSIS
    Checks that auditing and health monitoring is enabled for Microsoft Sentinel.
 
.DESCRIPTION
    Verifies that at least one Sentinel-onboarded Log Analytics workspace has an Azure Monitor
    diagnostic setting whose logs[] contains at least one entry with enabled == true and whose
    destination is a Log Analytics workspace (properties.workspaceId is non-empty).
    This is the documented mechanism for populating the SentinelAudit and SentinelHealth tables
    and for detecting detection-pipeline degradation and SOC tampering.
 
.NOTES
    Test ID: 41211
    Workshop Task: SECOPS_105
    Pillar: SecOps
    Category: Security information and event management
    Required API: Azure Resource Manager (management.azure.com), azure.insights diagnosticSettings
#>

function Test-Assessment-41211 {
    [ZtTest(
        Category = 'Security information and event management',
        ImplementationCost = 'Low',
        MinimumLicense = ('Consumption-based: Microsoft Sentinel'),
        Pillar = 'SecOps',
        RiskLevel = 'Medium',
        Service = ('Azure'),
        SfiPillar = 'Accelerate response and remediation',
        TenantType = ('Workforce'),
        TestId = 41211,
        Title = 'Auditing and health monitoring is enabled for Microsoft Sentinel',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection

    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    $activity = 'Checking Sentinel auditing and health monitoring diagnostic settings'

    # Q1 + Q2 + onboarding check delegated to the shared helper.
    # 'Forbidden' → 401/403 on ARG subscription or workspace query (spec: Investigate).
    # $null → unexpected ARG failure (spec: Investigate).
    # 'NoSubscriptions' → no enabled subscriptions accessible (spec: Skip).
    # 'NoWorkspaces' → no Log Analytics workspaces found (spec: Skip).
    # array → per-workspace results; PermissionError=$true means 401/403 on onboarding check.
    $allWorkspaces = Get-SentinelWorkspaceData -Activity $activity

    if ($null -eq $allWorkspaces) {
        $params = @{
            TestId       = '41211'
            Title        = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
            Status       = $false
            Result       = '⚠️ Azure Resource Graph returned an unexpected error while querying subscriptions or Log Analytics workspaces. This is likely a transient issue, please re-run the assessment.'
            CustomStatus = 'Investigate'
        }
        Add-ZtTestResultDetail @params
        return
    }

    if ($allWorkspaces -eq 'Forbidden') {
        $params = @{
            TestId       = '41211'
            Title        = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
            Status       = $false
            Result       = '⚠️ Azure Resource Graph returned insufficient permissions when querying subscriptions or workspaces. Ensure you have at least Reader access to the Azure subscriptions being tested.'
            CustomStatus = 'Investigate'
        }
        Add-ZtTestResultDetail @params
        return
    }

    if ($allWorkspaces -eq 'NoSubscriptions') {
        Write-PSFMessage 'No enabled subscriptions found — skipping Sentinel audit/health-monitoring check.' -Tag Test -Level VeryVerbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable
        return
    }

    if ($allWorkspaces -eq 'NoWorkspaces') {
        Write-PSFMessage 'No Log Analytics workspaces found across accessible subscriptions — skipping Sentinel audit/health-monitoring check.' -Tag Test -Level VeryVerbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable
        return
    }

    $checkableWorkspaces = @($allWorkspaces | Where-Object { -not $_.PermissionError })
    $forbiddenWorkspaces = @($allWorkspaces | Where-Object { $_.PermissionError })
    $onboardedWorkspaces = @($checkableWorkspaces | Where-Object { $_.SentinelOnboarded })

    if ($onboardedWorkspaces.Count -eq 0) {
        if ($forbiddenWorkspaces.Count -gt 0) {
            # Cannot confirm whether inaccessible workspaces have Sentinel onboarded.
            $params = @{
                TestId       = '41211'
                Title        = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
                Status       = $false
                Result       = '⚠️ One or more Log Analytics workspaces returned insufficient permissions when checking Sentinel onboarding state. No Sentinel-onboarded workspace was confirmed among accessible workspaces — the overall state cannot be determined. Ensure Microsoft Sentinel Reader is granted on all workspaces and re-run the assessment.'
                CustomStatus = 'Investigate'
            }
            Add-ZtTestResultDetail @params
        }
        else {
            # Spec: no Sentinel-onboarded workspaces → Skipped.
            Write-PSFMessage 'No Sentinel-onboarded workspaces found — skipping Sentinel audit/health-monitoring check.' -Tag Test -Level VeryVerbose
            Add-ZtTestResultDetail -SkippedBecause NotApplicable
        }
        return
    }

    # Q1: For each Sentinel-onboarded workspace, retrieve the list of diagnostic settings.
    # A setting qualifies when logs[].enabled == true and properties.workspaceId is non-empty
    # (confirming routing to a Log Analytics workspace rather than Event Hub or Storage Account).
    $diagSettingsByWorkspace = @{}

    foreach ($workspace in $onboardedWorkspaces) {
        Write-ZtProgress -Activity $activity -Status "Fetching diagnostic settings for workspace '$($workspace.WorkspaceName)' in '$($workspace.SubscriptionName)'"
        $diagPath = "$($workspace.WorkspaceId)/providers/microsoft.insights/diagnosticSettings?api-version=2021-05-01-preview"

        try {
            $diagSettingsByWorkspace[$workspace.WorkspaceId] = @(Invoke-ZtAzureRequest -Path $diagPath -ErrorAction Stop)
        }
        catch {
            $diagSettingsByWorkspace[$workspace.WorkspaceId] = $null
            Write-PSFMessage "Diagnostic settings API call failed — cannot determine setting state for workspace '$($workspace.WorkspaceName)' in subscription '$($workspace.SubscriptionName)': $_" -Tag Test -Level Warning
        }
    }

    #endregion Data Collection

    #region Assessment Logic

    $workspaceResults = foreach ($workspace in $onboardedWorkspaces) {
        $diagSettings = $diagSettingsByWorkspace[$workspace.WorkspaceId]

        $totalSettingCount = $null
        $settingDetails    = @()
        $rowStatus         = 'Fail'

        if ($null -eq $diagSettings) {
            # Diagnostic settings API call failed — cannot determine setting state for this workspace.
            $rowStatus = 'Investigate'
        }
        elseif ($diagSettings.Count -eq 0) {
            # Q1 returned an empty collection — no diagnostic settings configured (spec: Fail).
            $totalSettingCount = 0
            $rowStatus = 'Fail'
        }
        else {
            $totalSettingCount = $diagSettings.Count

            # Build per-setting detail for display, preserving the category-to-destination
            # association for each diagnostic setting. Include categoryGroup (e.g. allLogs, audit)
            # as fallback for logs[].category — both are valid shapes for a log entry; a given
            # entry uses one or the other, never both simultaneously.
            $settingDetails = @(foreach ($setting in $diagSettings) {
                $enabledLogs = @($setting.properties.logs | Where-Object { $_.enabled -eq $true })
                $settingCats = @($enabledLogs | ForEach-Object {
                    if ($_.category) { $_.category } else { $_.categoryGroup }
                } | Where-Object { $_ })
                [PSCustomObject]@{
                    Name              = $setting.name
                    EnabledCategories = $settingCats
                    DestinationId     = $setting.properties.workspaceId
                }
            })

            # A qualifying setting has at least one logs[] entry with enabled == true
            # AND routes to a Log Analytics workspace (properties.workspaceId non-empty).
            $qualifyingSettings = @($diagSettings | Where-Object {
                ($_.properties.logs | Where-Object { $_.enabled -eq $true }).Count -gt 0 -and
                -not [string]::IsNullOrEmpty($_.properties.workspaceId)
            })

            # Settings with enabled logs but no LAW destination — cannot confirm the feature is on.
            $enabledLogsNoLaw = @($diagSettings | Where-Object {
                ($_.properties.logs | Where-Object { $_.enabled -eq $true }).Count -gt 0 -and
                [string]::IsNullOrEmpty($_.properties.workspaceId)
            })

            if ($qualifyingSettings.Count -gt 0) {
                $rowStatus = 'Pass'
            }
            elseif ($enabledLogsNoLaw.Count -gt 0) {
                # Logs are enabled but destination is not a Log Analytics workspace (spec: Investigate).
                $rowStatus = 'Investigate'
            }
            else {
                # Settings exist but none have any logs[].enabled == true (spec: Investigate).
                $rowStatus = 'Investigate'
            }
        }

        [PSCustomObject]@{
            SubscriptionName   = $workspace.SubscriptionName
            SubscriptionId     = $workspace.SubscriptionId
            WorkspaceName      = $workspace.WorkspaceName
            ResourceGroup      = $workspace.ResourceGroup
            WorkspaceId        = $workspace.WorkspaceId
            TotalSettingCount  = $totalSettingCount
            DiagnosticSettings = $settingDetails  # array of per-setting objects; preserves category-to-destination association
            RowStatus          = $rowStatus
        }
    }
    $workspaceResults = @($workspaceResults)

    $passedItems      = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Pass' })
    $investigateItems = @($workspaceResults | Where-Object { $_.RowStatus -eq 'Investigate' })

    # Pass when at least one Sentinel workspace has a qualifying diagnostic setting.
    $passed       = $passedItems.Count -gt 0
    $customStatus = $null

    if (-not $passed -and ($investigateItems.Count -gt 0 -or $forbiddenWorkspaces.Count -gt 0)) {
        $customStatus       = 'Investigate'
        $testResultMarkdown = "⚠️ Auditing and health monitoring could not be confirmed for one or more Sentinel workspaces. This may be due to a diagnostic settings API failure, all log categories being disabled, or diagnostic settings routing to a non-Log Analytics destination. Re-run after verifying Monitoring Reader access on each affected workspace.`n`n%TestResult%"
    }
    elseif ($passed) {
        $testResultMarkdown = "✅ Auditing and health monitoring is enabled for the Sentinel workspace.`n`n%TestResult%"
    }
    else {
        $testResultMarkdown = "❌ Auditing and health monitoring is not enabled for the Sentinel workspace.`n`n%TestResult%"
    }

    #endregion Assessment Logic

    #region Report Generation

    $azContext          = Get-AzContext -ErrorAction SilentlyContinue
    $portalHost         = if ($azContext -and $azContext.Environment.Name -eq 'AzureUSGovernment') { 'https://portal.azure.us' } else { 'https://portal.azure.com' }
    $portalSentinelLink = "$portalHost/#view/HubsExtension/BrowseResource/resourceType/microsoft.securityinsightsarg%2Fsentinel"
    $tableTitle         = 'Auditing and health monitoring status per workspace'

    $formatTemplate = @'
 
 
## [{0}]({1})
 
| Subscription | Workspace | Diagnostic settings | Setting name | Enabled categories | Destination workspace | Status |
| :----------- | :-------- | ------------------: | :----------- | :----------------- | :-------------------- | :----- |
{2}
'@


    $tableRows      = ''
    $maxDisplay     = 10
    $statusPriority = @{ Fail = 0; Investigate = 1; Pass = 2 }
    $displayResults = @($workspaceResults | Sort-Object { $statusPriority[$_.RowStatus] }, SubscriptionName, WorkspaceName)
    $hasMoreItems   = $false
    if ($workspaceResults.Count -gt $maxDisplay) {
        $displayResults = @($displayResults | Select-Object -First $maxDisplay)
        $hasMoreItems   = $true
    }

    foreach ($result in $displayResults) {
        $subLink      = "$portalHost/#resource/subscriptions/$($result.SubscriptionId)"
        $diagLink     = "$portalHost/#resource$($result.WorkspaceId)/diagnosticSettings"
        $subMd        = "[$(Get-SafeMarkdown $result.SubscriptionName)]($subLink)"
        $workspaceMd  = "[$(Get-SafeMarkdown $result.WorkspaceName)]($diagLink)"
        $countMd      = if ($null -eq $result.TotalSettingCount) { '—' } else { $result.TotalSettingCount }

        if ($result.DiagnosticSettings.Count -gt 0) {
            # One row per diagnostic setting — preserves the category-to-destination association.
            foreach ($setting in $result.DiagnosticSettings) {
                $settingNameMd = Get-SafeMarkdown -Text $setting.Name
                $categoriesMd  = if ($setting.EnabledCategories.Count -gt 0) {
                    $setting.EnabledCategories -join ', '
                } else { '—' }
                $destMd        = if (-not [string]::IsNullOrEmpty($setting.DestinationId)) {
                    $wsName = ($setting.DestinationId -split '/')[-1]
                    "[$(Get-SafeMarkdown $wsName)]($portalHost/#resource$($setting.DestinationId)/overview)"
                } elseif ($setting.EnabledCategories.Count -gt 0) {
                    # Logs enabled but destination is not a Log Analytics workspace.
                    '⚠️ Non-LAW destination'
                } else { '—' }
                $settingStatus = if ($setting.EnabledCategories.Count -gt 0 -and -not [string]::IsNullOrEmpty($setting.DestinationId)) {
                    '✅ Active'
                } elseif ($setting.EnabledCategories.Count -gt 0) {
                    '⚠️ Non-LAW'
                } else {
                    '❌ Logs disabled'
                }
                $tableRows += "| $subMd | $workspaceMd | $countMd | $settingNameMd | $categoriesMd | $destMd | $settingStatus |`n"
            }
        } else {
            # No diagnostic settings (Fail) or API error (Investigate) — single placeholder row.
            $placeholderStatus = if ($result.RowStatus -eq 'Investigate') { '⚠️ Investigate' } else { '❌ No settings' }
            $tableRows += "| $subMd | $workspaceMd | $countMd | — | — | — | $placeholderStatus |`n"
        }
    }

    if ($hasMoreItems) {
        $remainingCount = $workspaceResults.Count - $maxDisplay
        $tableRows     += "`n... and $remainingCount more. [View all in Microsoft Sentinel]($portalSentinelLink)`n"
    }

    $mdInfo             = $formatTemplate -f $tableTitle, $portalSentinelLink, $tableRows
    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo

    #endregion Report Generation

    $params = @{
        TestId = '41211'
        Title  = 'Auditing and health monitoring is enabled for Microsoft Sentinel'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($null -ne $customStatus) {
        $params.CustomStatus = $customStatus
    }

    Add-ZtTestResultDetail @params
}