tests/Test-Assessment.41081.ps1

<#
.SYNOPSIS
    Checks that at least one Microsoft Defender for Cloud Apps anomaly detection policy is generating alerts.
 
.NOTES
    Test ID: 41081
    Workshop Task: SECOPS-081
    Pillar: SecOps
    Category: SaaS threat detection
    Required permission: SecurityAlert.Read.All
#>


function Test-Assessment-41081 {

    [ZtTest(
        Category = 'SaaS threat detection',
        CompatibleLicense = ('ADALLOM_S_STANDALONE','ADALLOM_S_O365'),
        ImplementationCost = 'Low',
        Pillar = 'SecOps',
        RiskLevel = 'High',
        Service = ('Graph'),
        SfiPillar = 'Monitor and detect cyberthreats',
        TenantType = ('Workforce'),
        TestId = 41081,
        Title = 'At least one Microsoft Defender for Cloud Apps anomaly detection policy is generating alerts',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param()

    #region Data Collection
    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
    $activity = 'Checking Microsoft Defender for Cloud Apps anomaly detection policy alerts'
    Write-ZtProgress -Activity $activity -Status 'Querying security alerts'

    $cutoff = (Get-Date).ToUniversalTime().AddDays(-90).ToString('yyyy-MM-ddTHH:mm:ssZ')
    $filter  = "serviceSource eq 'microsoftDefenderForCloudApps' and createdDateTime ge $cutoff"

    $allAlerts = $null
    try {
        $allAlerts = Invoke-ZtGraphRequest -RelativeUri 'security/alerts_v2' -Filter $filter -ApiVersion beta -ErrorAction Stop
    }
    catch {
        $httpStatus = Get-ZtHttpStatusCode -ErrorRecord $_
        if ($httpStatus -in @(401, 403)) {
            $params = @{
                TestId       = '41081'
                Title        = 'At least one Microsoft Defender for Cloud Apps anomaly detection policy is generating alerts'
                Status       = $false
                Result       = '⚠️ **SecurityAlert.Read.All** permission is required to read security alerts. Verify the permission is consented and re-run the assessment.'
                CustomStatus = 'Investigate'
            }
            Add-ZtTestResultDetail @params
            return
        }
        $params = @{
            TestId       = '41081'
            Title        = 'At least one Microsoft Defender for Cloud Apps anomaly detection policy is generating alerts'
            Status       = $false
            Result       = '⚠️ Transient Microsoft Graph error or unexpected response while querying security alerts; re-run after 5–10 minutes, file a support ticket if persistent.'
            CustomStatus = 'Investigate'
        }
        Add-ZtTestResultDetail @params
        return
    }
    #endregion Data Collection

    #region Assessment Logic
    $unfilteredAlerts = @($allAlerts)

    # Client-side filter: anomaly detection alerts only.
    $anomalyAlerts = @($unfilteredAlerts | Where-Object {
        $_.detectionSource -eq 'cloudAppSecurity' -and $null -ne $_.alertPolicyId -and $_.alertPolicyId -ne ''
    })

    $passed       = $false
    $customStatus = $null

    if ($anomalyAlerts.Count -ge 1) {
        $passed = $true
        $testResultMarkdown = "✅ Microsoft Defender for Cloud Apps anomaly detection policies are active and producing alerts.`n`n%TestResult%"
    } elseif ($unfilteredAlerts.Count -gt 0) {
        # MDA produced alerts (session policy, file policy, or App Governance) but none were
        # anomaly-detection-attributed; administrator review is required.
        $passed       = $false
        $customStatus = 'Investigate'
        $testResultMarkdown = "⚠️ Microsoft Defender for Cloud Apps returned alerts in the last 90 days but none have **detectionSource = cloudAppSecurity** with a non-null **alertPolicyId**. Review built-in anomaly detection policy state in the Microsoft Defender portal.`n`n%TestResult%"
    } else {
        $passed = $false
        $testResultMarkdown = "❌ No Microsoft Defender for Cloud Apps anomaly detection policy alerts were observed in the last 90 days.`n`n%TestResult%"
    }
    #endregion Assessment Logic

    #region Report Generation
    $portalUrl = 'https://security.microsoft.com/cloudapps/policies'

    $preTableLines = "## [Defender XDR > Cloud apps > Policies > Anomaly detection]($portalUrl)`n`n"

    # Only render the alert table when anomaly alerts were found (Pass case).
    if ($passed) {
        $totalCount  = $anomalyAlerts.Count
        $displayRows = @($anomalyAlerts | Select-Object -First 10)
        $isTruncated = $totalCount -gt 10

        $countLine = ''
        if ($isTruncated) {
            $countLine = "Total alerts: $totalCount (showing first 10)`n`n"
        }

        $tableRows = ''
        foreach ($alert in $displayRows) {
            $title           = Get-SafeMarkdown -Text $alert.title
            $severity        = if ($alert.severity)        { $alert.severity }        else { '—' }
            $detectionSource = if ($alert.detectionSource) { $alert.detectionSource } else { '—' }
            $created         = if ($alert.createdDateTime) { Get-FormattedDate -DateString $alert.createdDateTime } else { '—' }
            $alertStatus     = if ($alert.status)          { $alert.status }          else { '—' }
            $alertUrl        = if ($alert.alertWebUrl)     { "[$($alert.alertWebUrl)]($($alert.alertWebUrl))" } else { '—' }

            $tableRows += "| $title | $severity | $detectionSource | $created | $alertStatus | $alertUrl |`n"
        }

        if ($isTruncated) {
            $tableRows += "| ... | ... | ... | ... | ... | ... |`n"
        }

        $mdInfo = @"
`n$preTableLines
$countLine| Alert title | Severity | Detection source | Created | Status | Alert URL |
| :---------- | :------- | :--------------- | :------ | :----- | :-------- |
$tableRows
"@

    } else {
        # Investigate or Fail: just render the portal link, no table.
        $mdInfo = $preTableLines
    }

    $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
    #endregion Report Generation

    $params = @{
        TestId = '41081'
        Title  = 'At least one Microsoft Defender for Cloud Apps anomaly detection policy is generating alerts'
        Status = $passed
        Result = $testResultMarkdown
    }
    if ($customStatus) {
        $params.CustomStatus = $customStatus
    }
    Add-ZtTestResultDetail @params
}