tests/Test-Assessment.61004.ps1

<#
.SYNOPSIS
    Validates that Microsoft Defender for Cloud CSPM plan is enabled on all Azure subscriptions.
 
.DESCRIPTION
    This test evaluates the Defender for Cloud CloudPosture pricing tier for every enabled
    Azure subscription. A subscription passes only when pricingTier is 'Standard'.
    Without the CSPM plan active, AI posture recommendations, attack-path visibility, and
    AI Security Posture features cannot run on the subscription.
 
.NOTES
    Test ID: 61004
    Category: AI Cloud Posture
    Required APIs: Azure Resource Graph (resourcecontainers/subscriptions),
                   Azure Management REST API (Microsoft.Security/pricings/CloudPosture)
#>


function Test-Assessment-61004 {

    [ZtTest(
        Category = 'AI Cloud Posture',
        ImplementationCost = 'Medium',
        Service = ('Azure'),
        MinimumLicense = ('Microsoft_Defender_for_Cloud'),
        Pillar = 'AI',
        RiskLevel = 'High',
        SfiPillar = 'Protect tenants and production systems',
        TenantType = ('Workforce'),
        TestId = 61004,
        Title = 'Microsoft Defender for Cloud CSPM plan is enabled on all Azure subscriptions',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection

    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    $activity = 'Evaluating Microsoft Defender for Cloud CSPM plan configuration'

    # Q1: Enumerate all enabled subscriptions via Azure Resource Graph
    Write-ZtProgress -Activity $activity -Status 'Querying enabled subscriptions via Resource Graph'

    $argQuery = @"
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions'
| where properties.state =~ 'Enabled'
| project subscriptionId, displayName=name
"@


    $allSubscriptions = @()
    try {
        $allSubscriptions = @(Invoke-ZtAzureResourceGraphRequest -Query $argQuery)
        Write-PSFMessage "ARG Query returned $($allSubscriptions.Count) enabled subscription(s)" -Tag Test -Level VeryVerbose
    }
    catch {
        Write-PSFMessage "Azure Resource Graph query failed: $($_.Exception.Message)" -Tag Test -Level Warning
        Add-ZtTestResultDetail -SkippedBecause NotSupported
        return
    }

    if ($allSubscriptions.Count -eq 0) {
        Write-PSFMessage 'No enabled Azure subscriptions found.' -Tag Test -Level VeryVerbose
        Add-ZtTestResultDetail -SkippedBecause NotApplicable
        return
    }

    # Q2: For each subscription, read the Defender for Cloud CloudPosture pricing plan
    Write-ZtProgress -Activity $activity -Status 'Querying Defender for Cloud CSPM plan per subscription'

    $evaluationResults = @()

    foreach ($subscription in $allSubscriptions) {
        $subscriptionId = $subscription.subscriptionId
        $displayName    = $subscription.displayName

        $pricingPath = "/subscriptions/$subscriptionId/providers/Microsoft.Security/pricings/CloudPosture?api-version=2024-01-01"
        $pricingTier = 'Not configured'
        $rowStatus   = 'Fail'

        try {
            $pricingResponse = Invoke-ZtAzureRequest -Path $pricingPath
            if ($null -ne $pricingResponse -and $null -ne $pricingResponse.properties.pricingTier) {
                $pricingTier = $pricingResponse.properties.pricingTier
                $rowStatus   = if ($pricingTier -eq 'Standard') { 'Pass' } else { 'Fail' }
            }
        }
        catch {
            $httpStatusCode = $null
            if ($_.Exception.Message -match 'with status (\d+):') {
                $httpStatusCode = [int]$Matches[1]
            }

            if ($httpStatusCode -eq 404) {
                # 404 means the plan has never been configured on this subscription; treat as Fail per spec
                $pricingTier = 'Not configured'
                $rowStatus   = 'Fail'
            }
            elseif ($httpStatusCode -in @(401, 403)) {
                $pricingTier = 'Access denied'
                $rowStatus   = 'Investigate'
            }
            else {
                $pricingTier = 'Error'
                $rowStatus   = 'Fail'
            }
            Write-PSFMessage "Error querying Defender CSPM plan for subscription '$displayName' ($subscriptionId): $_" -Tag Test -Level Warning
        }

        $evaluationResults += [PSCustomObject]@{
            SubscriptionId = $subscriptionId
            DisplayName    = $displayName
            PricingTier    = $pricingTier
            RowStatus      = $rowStatus
        }
    }

    #endregion Data Collection

    #region Assessment Logic

    $failedItems      = @($evaluationResults | Where-Object { $_.RowStatus -eq 'Fail' })
    $investigateItems = @($evaluationResults | Where-Object { $_.RowStatus -eq 'Investigate' })
    $passed = ($failedItems.Count -eq 0) -and ($investigateItems.Count -eq 0)
    $customStatus = $null

    if ($investigateItems.Count -gt 0 -and $failedItems.Count -eq 0) {
        $customStatus = 'Investigate'
        $testResultMarkdown = "⚠️ Test failed to evaluate for one or more Azure Subscriptions. Make sure the account used during Connect-ZtAssessment has at least Security Reader access to Azure Subscriptions tested.`n`n%TestResult%"
    }
    elseif ($passed) {
        $testResultMarkdown = "✅ Microsoft Defender for Cloud CSPM plan (Standard tier) is enabled on every in-scope Azure subscription.`n`n%TestResult%"
    }
    else {
        $testResultMarkdown = "❌ One or more in-scope Azure subscriptions do not have the Defender CSPM plan enabled.`n`n%TestResult%"
    }

    #endregion Assessment Logic

    #region Report Generation

    $portalCspmLink             = 'https://portal.azure.com/#view/Microsoft_Azure_Security/SecurityMenuBlade/~/EnvironmentSettings'
    $portalSubPricingBaseLink    = 'https://portal.azure.com/#view/Microsoft_Azure_Security/PolicyMenuBlade/~/pricingTier/subscriptionId'
    $tableTitle = 'Subscriptions missing Defender CSPM plan'

    $formatTemplate = @'
 
 
## [{0}]({1})
 
| Subscription | Pricing tier | Status |
| :----------- | :----------- | :----- |
{2}
'@


    $nonPassItems = @($failedItems) + @($investigateItems)
    $mdInfo = ''
    if ($nonPassItems.Count -gt 0) {
        $tableRows         = ''
        $maxItemsToDisplay = 10
        $statusPriority    = @{ Fail = 0; Investigate = 1 }
        $displayResults    = @($nonPassItems | Sort-Object { $statusPriority[$_.RowStatus] }, DisplayName)
        $hasMoreItems      = $false
        if ($displayResults.Count -gt $maxItemsToDisplay) {
            $displayResults = @($displayResults | Select-Object -First $maxItemsToDisplay)
            $hasMoreItems   = $true
        }

        foreach ($result in $displayResults) {
            $displayNameLink = "[$(Get-SafeMarkdown $result.DisplayName)]($portalSubPricingBaseLink/$($result.SubscriptionId))"
            $pricingTierSafe = $result.PricingTier
            $statusDisplay   = switch ($result.RowStatus) {
                'Fail'        { '❌ Fail' }
                'Investigate' { '⚠️ Investigate' }
            }
            $tableRows      += "| $displayNameLink | $pricingTierSafe | $statusDisplay |`n"
        }

        if ($hasMoreItems) {
            $remainingCount = $nonPassItems.Count - $maxItemsToDisplay
            $tableRows += "`n... and $remainingCount more. [View all subscriptions in Microsoft Defender for Cloud]($portalCspmLink)`n"
        }

        $mdInfo = $formatTemplate -f $tableTitle, $portalCspmLink, $tableRows
    }

    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo

    #endregion Report Generation

    $params = @{
        TestId = '61004'
        Title  = 'Microsoft Defender for Cloud CSPM plan is enabled on all Azure subscriptions'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }

    Add-ZtTestResultDetail @params
}