tests/Test-Assessment.61024.ps1

<#
.SYNOPSIS
    Checks that the Microsoft Defender XDR Content Hub solution is installed on the Microsoft Sentinel workspace.
 
.DESCRIPTION
    Verifies that the Microsoft Defender XDR solution (or its legacy name 'Microsoft 365 Defender') is
    installed from the Content Hub on at least one Microsoft Sentinel-onboarded Log Analytics workspace.
 
.NOTES
    Test ID: 61024
    Category: AI Threat Detection
    Pillar: AI
    Required API: Azure Resource Manager (management.azure.com)
#>


function Test-Assessment-61024 {
    [ZtTest(
        Category = 'AI Threat Detection',
        MinimumLicense = ('WIN_DEF_ATP', 'MDATP_XPLAT', 'ATA', 'ADALLOM_STANDALONE', 'THREAT_INTELLIGENCE', 'IDENTITY_THREAT_PROTECTION', 'IDENTITY_THREAT_PROTECTION_FOR_EMS_E5', 'SPE_E5', 'Consumption-based: Microsoft Sentinel'),
        CompatibleLicense = ('WIN_DEF_ATP', 'MDATP_XPLAT', 'ATA', 'ADALLOM_STANDALONE', 'THREAT_INTELLIGENCE', 'IDENTITY_THREAT_PROTECTION', 'IDENTITY_THREAT_PROTECTION_FOR_EMS_E5', 'SPE_E5'),
        ImplementationCost = 'Low',
        Pillar = 'AI',
        RiskLevel = 'High',
        Service = ('Azure'),
        SfiPillar = 'Monitor and detect cyberthreats',
        TenantType = ('Workforce'),
        TestId = 61024,
        Title = 'Microsoft Defender XDR (unified) data connector is enabled on the Microsoft Sentinel workspace',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection

    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    $activity = 'Checking Microsoft Defender XDR Content Hub solution on Sentinel workspaces'

    # Q1 + Q2 + onboarding check.
    # Returns 'Forbidden' on ARG 401/403, $null on unexpected failure,
    # 'NoSubscriptions' / 'NoWorkspaces' when nothing is in scope.
    $allWorkspaces = Get-SentinelWorkspaceData -Activity $activity
    if ($null -eq $allWorkspaces) {
        $params = @{
            TestId       = '61024'
            Title        = 'Microsoft Defender XDR (unified) data connector is enabled on the Microsoft Sentinel workspace'
            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       = '61024'
            Title        = 'Microsoft Defender XDR (unified) data connector is enabled on the Microsoft Sentinel workspace'
            Status       = $false
            Result       = '⚠️ Azure Resource Graph returned insufficient permissions when enumerating subscriptions or workspaces. Ensure you have at least Reader access to the Azure subscriptions being tested.'
            CustomStatus = 'Investigate'
        }
        Add-ZtTestResultDetail @params
        return
    }

    # No enabled subscriptions accessible to the caller.
    if ($allWorkspaces -eq 'NoSubscriptions') {
        Write-PSFMessage 'No enabled subscriptions found — skipping Sentinel onboarding check.' -Tag Test -Level VeryVerbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable
        return
    }

    # No Log Analytics workspaces across accessible subscriptions.
    if ($allWorkspaces -eq 'NoWorkspaces') {
        Write-PSFMessage 'No Log Analytics workspaces found across accessible subscriptions — skipping Sentinel onboarding 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) {
            # Auth errors on the Sentinel onboarding check mean we cannot confirm whether
            # those workspaces are onboarded — we cannot rule out a passing workspace exists.
            $params = @{
                TestId       = '61024'
                Title        = 'Microsoft Defender XDR (unified) data connector is enabled on the Microsoft Sentinel workspace'
                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'
            }
        }
        else {
            # Full visibility, no workspace has Sentinel onboarded.
            $params = @{
                TestId = '61024'
                Title  = 'Microsoft Defender XDR (unified) data connector is enabled on the Microsoft Sentinel workspace'
                Status = $false
                Result = '❌ No Sentinel-onboarded workspace in tenant.'
            }
        }
        Add-ZtTestResultDetail @params
        return
    }

    $workspaceResults = @()

    foreach ($workspace in $onboardedWorkspaces) {
        $packageName = 'Not found'
        $rowStatus   = 'Fail'

        # Q3: List installed Content Hub solutions and look for Microsoft Defender XDR (or legacy Microsoft 365 Defender)
        Write-ZtProgress -Activity $activity -Status "Checking content packages on $($workspace.WorkspaceName) in subscription $($workspace.SubscriptionName)"
        $packagesPath = "$($workspace.WorkspaceId)/providers/Microsoft.SecurityInsights/contentPackages?api-version=2024-09-01"

        try {
            $packages   = @(Invoke-ZtAzureRequest -Path $packagesPath -ErrorAction Stop)
            $xdrPackage = $packages | Where-Object {
                $_.properties.displayName -ieq 'Microsoft Defender XDR' -or
                $_.properties.displayName -ieq 'Microsoft 365 Defender'
            } | Select-Object -First 1
            if ($xdrPackage) {
                $packageName = $xdrPackage.properties.displayName
                $rowStatus   = 'Pass'
            }
        }
        catch {
            $packageName = 'Error'
            $rowStatus   = 'Investigate'
            Write-PSFMessage "Error querying content packages for workspace '$($workspace.WorkspaceName)' in subscription '$($workspace.SubscriptionName)': $_" -Tag Test -Level Warning
        }

        $workspaceResults += [PSCustomObject]@{
            SubscriptionName = $workspace.SubscriptionName
            SubscriptionId   = $workspace.SubscriptionId
            WorkspaceName    = $workspace.WorkspaceName
            ResourceGroup    = $workspace.ResourceGroup
            WorkspaceId      = $workspace.WorkspaceId
            PackageName      = $packageName
            RowStatus        = $rowStatus
        }
    }

    #endregion Data Collection

    #region Assessment Logic

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

    $passed       = $passedItems.Count -gt 0
    $customStatus = $null

    if (-not $passed -and $investigateItems.Count -gt 0) {
        $customStatus       = 'Investigate'
        $testResultMarkdown = "⚠️ The Content Hub API returned an authorization (401/403) or transient (5xx) error on at least one workspace, so the Microsoft Defender XDR connector state could not be determined for those workspaces. Re-run after verifying Microsoft Sentinel Reader on each affected workspace's resource group.`n`n%TestResult%"
    }
    elseif ($passed) {
        $testResultMarkdown = "✅ The Microsoft Defender XDR data connector is enabled on at least one Sentinel-onboarded workspace.`n`n%TestResult%"
    }
    else {
        $testResultMarkdown = "❌ The Microsoft Defender XDR data connector is not enabled on any Sentinel-onboarded workspace.`n`n%TestResult%"
    }

    #endregion Assessment Logic

    #region Report Generation

    $portalSentinelLink = 'https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/microsoft.securityinsightsarg%2Fsentinel'
    $tableTitle         = 'Sentinel data connectors per workspace'

    $formatTemplate = @'
 
 
## [{0}]({1})
 
| Subscription | Workspace | Content package | 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       = "https://portal.azure.com/#resource/subscriptions/$($result.SubscriptionId)"
        $sentinelId    = "/subscriptions/$($result.SubscriptionId)/resourcegroups/$($result.ResourceGroup)/providers/microsoft.securityinsightsarg/sentinel/$($result.WorkspaceName)"
        $workspaceLink = "https://portal.azure.com/#view/Microsoft_Azure_Security_Insights/MainMenuBlade/~/DataConnectors/id/$($sentinelId -replace '/', '%2F')"
        $subMd         = "[$(Get-SafeMarkdown $result.SubscriptionName)]($subLink)"
        $workspaceMd   = "[$(Get-SafeMarkdown $result.WorkspaceName)]($workspaceLink)"
        $packageMd     = $result.PackageName
        $statusDisplay = switch ($result.RowStatus) {
            'Pass'        { '✅ Pass' }
            'Fail'        { '❌ Fail' }
            'Investigate' { '⚠️ Investigate' }
        }
        $tableRows += "| $subMd | $workspaceMd | $packageMd | $statusDisplay |`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 = '61024'
        Title  = 'Microsoft Defender XDR (unified) data connector is enabled on the Microsoft Sentinel workspace'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }

    Add-ZtTestResultDetail @params
}