Analyze/tests/03-DefenderForCloud.ps1

#Microsoft Defender for Cloud: plans, components, notifications and alerts

function Get-DefenderPricing {
    param([string]$Name)
    return @(Get-IngestData 'defender/pricings') | Where-Object { $_ -and $_.name -eq $Name } | Select-Object -First 1
}

function Get-PricingExtension {
    param($Pricing, [string]$Name)
    return @($Pricing.properties.extensions) | Where-Object { $_ -and $_.name -eq $Name } | Select-Object -First 1
}

#one test per plan; Override returns $true/$false when a resource enables or disables protection itself, $null when it follows the plan
$defenderPlans = @(
    @{ Id = 'AZ-DEF-001'; Plan = 'CloudPosture'; Name = 'Defender CSPM'; Cis = '8.1.1.1'; Mcsb = @('LT-1', 'PV-5'); Severity = 'Medium'; Always = $true
        Policy = @{ '1f90fc71-a595-4066-8974-d4d0802e8ef0' = 'Microsoft Defender CSPM should be enabled' }
        Why = 'Defender CSPM adds attack path analysis, the cloud security explorer, agentless scanning and data security posture management on top of the free foundational posture.' }
    @{ Id = 'AZ-DEF-002'; Plan = 'VirtualMachines'; Name = 'Defender for Servers'; Cis = '8.1.3.1'; Mcsb = @('LT-1', 'ES-1'); Severity = 'High'
        Types = @('Microsoft.Compute/virtualMachines', 'Microsoft.Compute/virtualMachineScaleSets', 'Microsoft.HybridCompute/machines')
        Policy = @{ '4da35fc9-c9e7-4960-aec9-797fe7d9051d' = 'Azure Defender for servers should be enabled' }
        Why = 'Defender for Servers provides Microsoft Defender for Endpoint (EDR), vulnerability management and threat detection for virtual machines and Arc machines.' }
    @{ Id = 'AZ-DEF-003'; Plan = 'Containers'; Name = 'Defender for Containers'; Cis = '8.1.4.1'; Mcsb = @('LT-1'); Severity = 'High'; Legacy = @('KubernetesService', 'ContainerRegistry')
        Types = @('Microsoft.ContainerService/managedClusters', 'Microsoft.ContainerRegistry/registries', 'Microsoft.Kubernetes/connectedClusters')
        Policy = @{ '1c988dd6-ade4-430f-a608-2a3e5b0a6d38' = 'Microsoft Defender for Containers should be enabled' }
        Why = 'Defender for Containers provides runtime threat detection for Kubernetes and vulnerability assessment of container images.' }
    @{ Id = 'AZ-DEF-004'; Plan = 'StorageAccounts'; Name = 'Defender for Storage'; Cis = '8.1.5.1'; Mcsb = @('LT-1', 'DP-2'); Severity = 'High'
        Types = @('Microsoft.Storage/storageAccounts')
        Policy = @{ '640d2586-54d2-465f-877f-9ffc1d2109f4' = 'Microsoft Defender for Storage should be enabled' }
        Why = 'Defender for Storage detects unusual access, data exfiltration and malware uploads to storage accounts.'
        Override = {
            param($Record)
            $setting = Get-Child $Record 'providers/Microsoft.Security/defenderForStorageSettings/current'
            if ($setting -and $setting.properties.overrideSubscriptionLevelSettings) { return [bool]$setting.properties.isEnabled }
            return $null
        } }
    @{ Id = 'AZ-DEF-005'; Plan = 'AppServices'; Name = 'Defender for App Service'; Cis = '8.1.6.1'; Mcsb = @('LT-1'); Severity = 'Medium'
        Types = @('Microsoft.Web/sites')
        Policy = @{ '2913021d-f2fd-4f3d-b958-22354e2bdbcb' = 'Azure Defender for App Service should be enabled' }
        Why = 'Defender for App Service detects attacks against web applications and dangling DNS entries of decommissioned apps.' }
    @{ Id = 'AZ-DEF-006'; Plan = 'CosmosDbs'; Name = 'Defender for Azure Cosmos DB'; Cis = '8.1.7.1'; Mcsb = @('LT-1', 'DP-2'); Severity = 'Medium'
        Types = @('Microsoft.DocumentDB/databaseAccounts')
        Policy = @{ 'adbe85b5-83e6-4350-ab58-bf3a4f736e5e' = 'Microsoft Defender for Azure Cosmos DB should be enabled' }
        Why = 'Defender for Azure Cosmos DB detects SQL injection, anomalous access and data exfiltration attempts.'
        Override = {
            param($Record)
            $setting = Get-Child $Record 'providers/Microsoft.Security/advancedThreatProtectionSettings/current'
            if ($setting -and $setting.properties.isEnabled) { return $true }
            return $null
        } }
    @{ Id = 'AZ-DEF-007'; Plan = 'OpenSourceRelationalDatabases'; Name = 'Defender for open-source relational databases'; Cis = '8.1.7.2'; Mcsb = @('LT-1', 'DP-2'); Severity = 'Medium'
        Types = @('Microsoft.DBforPostgreSQL/flexibleServers', 'Microsoft.DBforPostgreSQL/servers', 'Microsoft.DBforMySQL/flexibleServers', 'Microsoft.DBforMySQL/servers')
        Policy = @{ '0a9fbe0d-c5c4-4da8-87d8-f4fd77338835' = 'Azure Defender for open-source relational databases should be enabled' }
        Why = 'Defender for open-source relational databases detects brute force, anomalous access and suspicious queries on PostgreSQL and MySQL servers.'
        Override = {
            param($Record)
            $setting = @(Get-Child $Record 'advancedThreatProtectionSettings') | Where-Object { $_ } | Select-Object -First 1
            if ($setting -and $setting.properties.state -eq 'Enabled') { return $true }
            return $null
        } }
    @{ Id = 'AZ-DEF-008'; Plan = 'SqlServers'; Name = 'Defender for Azure SQL'; Cis = '8.1.7.3'; Mcsb = @('LT-1', 'DP-2'); Severity = 'High'
        Types = @('Microsoft.Sql/servers', 'Microsoft.Sql/managedInstances', 'Microsoft.Synapse/workspaces')
        Policy = @{ '7fe3b40f-802b-4cdd-8bd4-fd799c948cc2' = 'Azure Defender for Azure SQL Database servers should be enabled'; 'abfb4388-5bf4-4ad7-ba82-2cd2f41ceae9' = 'Azure Defender for SQL should be enabled for unprotected Azure SQL servers'; 'abfb7388-5bf4-4ad7-ba99-2cd2f41cebb9' = 'Azure Defender for SQL should be enabled for unprotected SQL Managed Instances' }
        Why = 'Defender for SQL provides vulnerability assessment and detects SQL injection, brute force and anomalous database access.'
        Override = {
            param($Record)
            $setting = @(Get-Child $Record 'advancedThreatProtectionSettings') + @(Get-Child $Record 'securityAlertPolicies') | Where-Object { $_ -and $_.properties.state -eq 'Enabled' } | Select-Object -First 1
            if ($setting) { return $true }
            return $null
        } }
    @{ Id = 'AZ-DEF-009'; Plan = 'SqlServerVirtualMachines'; Name = 'Defender for SQL servers on machines'; Cis = '8.1.7.4'; Mcsb = @('LT-1', 'DP-2'); Severity = 'Medium'
        Types = @('Microsoft.SqlVirtualMachine/sqlVirtualMachines', 'Microsoft.AzureArcData/sqlServerInstances')
        Policy = @{ '6581d072-105e-4418-827f-bd446d56421b' = 'Azure Defender for SQL servers on machines should be enabled' }
        Why = 'Defender for SQL servers on machines protects SQL Server running on virtual machines and Arc enabled servers.' }
    @{ Id = 'AZ-DEF-010'; Plan = 'KeyVaults'; Name = 'Defender for Key Vault'; Cis = '8.1.8.1'; Mcsb = @('LT-1', 'DP-8'); Severity = 'Medium'
        Types = @('Microsoft.KeyVault/vaults')
        Policy = @{ '0e6763cc-5078-4e64-889d-ff4d9a839047' = 'Azure Defender for Key Vault should be enabled' }
        Why = 'Defender for Key Vault detects unusual and potentially harmful access to secrets, keys and certificates.' }
    @{ Id = 'AZ-DEF-011'; Plan = 'Arm'; Name = 'Defender for Resource Manager'; Cis = '8.1.9.1'; Mcsb = @('LT-1', 'LT-2'); Severity = 'Medium'; Always = $true
        Policy = @{ 'c3d20c29-b36d-48fe-808b-99a87530ad99' = 'Azure Defender for Resource Manager should be enabled' }
        Why = 'Defender for Resource Manager detects suspicious management operations such as the use of exploitation toolkits, unusual role assignments and suspicious control plane access.' }
    @{ Id = 'AZ-DEF-012'; Plan = 'Api'; Name = 'Defender for APIs'; Cis = '8.1.2.1'; Mcsb = @('LT-1'); Severity = 'Low'
        Types = @('Microsoft.ApiManagement/service')
        Policy = @{ '7926a6d1-b268-4586-8197-e8ae90c877d7' = 'Microsoft Defender for APIs should be enabled' }
        Why = 'Defender for APIs inventories APIs published through API Management and detects attacks against them.' }
    @{ Id = 'AZ-DEF-013'; Plan = 'AI'; Name = 'Defender for AI services'; Mcsb = @('AI-6', 'LT-1'); Severity = 'Medium'
        Types = @('Microsoft.CognitiveServices/accounts')
        Why = 'Defender for AI services detects prompt injection (jailbreak), data leakage and credential theft attempts against Azure OpenAI and AI services.' }
)

foreach ($plan in $defenderPlans) {
    $frameworks = @{ MCSB = $plan.Mcsb; WAF = 'SE:10'; ALZ = 'Deploy-MDFC-Config-H224' }
    if ($plan.Cis) { $frameworks.CIS = $plan.Cis }
    Add-AzTest @{
        Id          = $plan.Id
        Title       = "Microsoft $($plan.Name) is enabled"
        Category    = 'Logging and threat detection'
        Service     = 'Microsoft Defender for Cloud'
        Severity    = $plan.Severity
        Description = if ($plan.Always) { "Checks that the $($plan.Name) plan is enabled on the subscription." } else { "Checks that the $($plan.Name) plan is enabled, and that each resource it protects is covered by the plan or by protection enabled on the resource itself." }
        Rationale   = $plan.Why
        Remediation = "Enable the plan in Defender for Cloud > Environment settings > <subscription> > Defender plans (az security pricing create --name $($plan.Plan) --tier Standard)."
        References  = @('https://learn.microsoft.com/azure/defender-for-cloud/defender-for-cloud-introduction')
        Frameworks  = $frameworks
        Policy      = $plan.Policy
        Requires    = @('defender/pricings')
        Config      = $plan
        Run         = {
            param($Test)
            $planCopy = $Test.Config
            $pricing = Get-DefenderPricing $planCopy.Plan
            $standard = $pricing.properties.pricingTier -eq 'Standard'
            $legacy = @($planCopy.Legacy | Where-Object { $_ } | Where-Object { (Get-DefenderPricing $_).properties.pricingTier -eq 'Standard' })
            $evidence = [ordered]@{ plan = $planCopy.Plan; pricingTier = $pricing.properties.pricingTier; subPlan = $pricing.properties.subPlan; enabledLegacyPlans = $legacy }
            if (-not $pricing) { return New-SubscriptionFinding (New-Unknown "Plan $($planCopy.Plan) was not returned by the pricings API" $evidence) }
            if ($planCopy.Always) {
                if ($standard) { return New-SubscriptionFinding (New-Pass "$($planCopy.Name) is enabled" $evidence) }
                return New-SubscriptionFinding (New-Fail "$($planCopy.Name) is not enabled" $evidence)
            }
            $records = @(Get-AzResourceRecords -Type $planCopy.Types)
            if (-not $records) {
                if ($standard) { return New-SubscriptionFinding (New-Pass "$($planCopy.Name) is enabled (no resources in scope yet)" $evidence) }
                return New-SubscriptionFinding (New-NotApplicable "$($planCopy.Name) is off and there are no resources it would protect" $evidence)
            }
            foreach ($record in $records) {
                $override = if ($planCopy.Override) { & $planCopy.Override $record } else { $null }
                $resourceEvidence = [ordered]@{ planTier = $pricing.properties.pricingTier; resourceLevelProtection = $override }
                $result = if ($override -eq $false) { New-Fail 'Protection is disabled on the resource, overriding the subscription plan' $resourceEvidence }
                elseif ($standard -or $legacy) { New-Pass "Protected by the $($planCopy.Name) plan" $resourceEvidence }
                elseif ($override -eq $true) { New-Pass 'Protection is enabled on the resource' $resourceEvidence }
                else { New-Fail "Not protected: the $($planCopy.Name) plan is off" $resourceEvidence }
                New-Finding -Record $record -Result $result
            }
        }
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-014'
    Title       = 'Defender for Servers endpoint protection (Defender for Endpoint integration) is on'
    Category    = 'Endpoint security'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'High'
    Description = 'Checks the WDATP integration setting that deploys Microsoft Defender for Endpoint to machines protected by Defender for Servers.'
    Rationale   = 'Endpoint detection and response on servers is the primary control against malware, ransomware and hands-on-keyboard attacks; without the integration the Servers plan does not deploy it.'
    Remediation = "Enable 'Endpoint protection' under Defender plans > Servers > Settings (security setting WDATP set to enabled)."
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/integration-defender-for-endpoint')
    Frameworks  = @{ MCSB = @('ES-1', 'ES-2'); CIS = '8.1.3.3'; ALZ = 'Deploy-MDEndpoints' }
    Requires    = @('defender/settings', 'defender/pricings')
    Run         = {
        $setting = @(Get-IngestData 'defender/settings') | Where-Object { $_ -and $_.name -eq 'WDATP' } | Select-Object -First 1
        $servers = (Get-DefenderPricing 'VirtualMachines').properties.pricingTier
        $evidence = [ordered]@{ wdatpEnabled = [bool]$setting.properties.enabled; serversPlan = $servers }
        if (-not $setting) { return New-SubscriptionFinding (New-Unknown 'The WDATP setting was not returned' $evidence) }
        if (-not $setting.properties.enabled) { return New-SubscriptionFinding (New-Fail 'Defender for Endpoint integration is off' $evidence) }
        if ($servers -ne 'Standard') { return New-SubscriptionFinding (New-Fail 'Defender for Endpoint integration is on but Defender for Servers is off, so nothing is deployed' $evidence) }
        New-SubscriptionFinding (New-Pass 'Defender for Endpoint integration is on' $evidence)
    }
}

$defenderServerComponents = @(
    @{ Id = 'AZ-DEF-015'; Extension = 'AgentlessVmScanning'; Name = 'Agentless scanning for machines'; Cis = '8.1.3.4'; Mcsb = @('PV-5'); Severity = 'Medium'
        Why = 'Agentless scanning inspects disk snapshots for vulnerabilities, secrets and malware without an agent, including machines where agents are missing or broken.' }
    @{ Id = 'AZ-DEF-016'; Extension = 'FileIntegrityMonitoring'; Name = 'File integrity monitoring'; Cis = '8.1.3.5'; Mcsb = @('PV-4', 'LT-1'); Severity = 'Low'
        Why = 'File integrity monitoring detects changes to operating system files, registry keys and application binaries that indicate compromise or unauthorized change.' }
)
foreach ($component in $defenderServerComponents) {
    Add-AzTest @{
        Id          = $component.Id
        Title       = "Defender for Servers component '$($component.Name)' is on"
        Category    = if ($component.Mcsb[0] -like 'PV-*') { 'Posture and vulnerability management' } else { 'Logging and threat detection' }
        Service     = 'Microsoft Defender for Cloud'
        Severity    = $component.Severity
        Description = "Checks that the $($component.Name) extension of the Defender for Servers plan (or Defender CSPM for agentless scanning) is enabled."
        Rationale   = $component.Why
        Remediation = "Enable '$($component.Name)' under Defender plans > Servers > Settings (requires Defender for Servers Plan 2)."
        References  = @('https://learn.microsoft.com/azure/defender-for-cloud/defender-for-servers-overview')
        Frameworks  = @{ MCSB = $component.Mcsb; CIS = $component.Cis }
        Requires    = @('defender/pricings')
        Config      = $component
        Run         = {
            param($Test)
            $componentCopy = $Test.Config
            $sources = @('VirtualMachines')
            if ($componentCopy.Extension -eq 'AgentlessVmScanning') { $sources += 'CloudPosture' }
            $enabledIn = @($sources | Where-Object {
                    $pricing = Get-DefenderPricing $_
                    $extension = Get-PricingExtension $pricing $componentCopy.Extension
                    $pricing.properties.pricingTier -eq 'Standard' -and $extension -and $extension.isEnabled -eq 'True'
                })
            $evidence = [ordered]@{ enabledIn = $enabledIn; serversPlan = (Get-DefenderPricing 'VirtualMachines').properties.pricingTier; serversSubPlan = (Get-DefenderPricing 'VirtualMachines').properties.subPlan }
            if ($enabledIn) { return New-SubscriptionFinding (New-Pass "$($componentCopy.Name) is on ($($enabledIn -join ', '))" $evidence) }
            New-SubscriptionFinding (New-Fail "$($componentCopy.Name) is off" $evidence)
        }
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-017'
    Version     = 2
    Title       = 'Vulnerability assessment for machines is configured'
    Category    = 'Posture and vulnerability management'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'Medium'
    Description = 'Checks that Defender for Servers is on and that a vulnerability assessment provider (Microsoft Defender Vulnerability Management) covers machines. Both Plan 1 and Plan 2 include Defender Vulnerability Management.'
    Rationale   = 'Unknown vulnerabilities cannot be prioritized or patched. Built-in vulnerability management continuously finds missing patches and vulnerable software on servers.'
    Remediation = "Enable Defender for Servers and set 'Vulnerability assessment for machines' to Microsoft Defender Vulnerability Management."
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/deploy-vulnerability-assessment-defender-vulnerability-management')
    Frameworks  = @{ MCSB = @('PV-5'); CIS = '8.1.3.2' }
    Policy      = @{ '501541f7-f7e7-4cd6-868c-4190fdad3ac9' = 'A vulnerability assessment solution should be enabled on your virtual machines' }
    Requires    = @('defender/pricings')
    Run         = {
        $servers = Get-DefenderPricing 'VirtualMachines'
        $evidence = [ordered]@{ serversPlan = $servers.properties.pricingTier; serversSubPlan = $servers.properties.subPlan }
        if ($servers.properties.pricingTier -ne 'Standard') { return New-SubscriptionFinding (New-Fail 'Defender for Servers is off, so no vulnerability assessment runs' $evidence) }
        #both Defender for Servers plans include Defender Vulnerability Management, so the plan itself already satisfies this
        if ($servers.properties.subPlan -in 'P1', 'P2') { return New-SubscriptionFinding (New-Pass "Defender for Servers $($servers.properties.subPlan) includes Defender Vulnerability Management" $evidence) }
        if (-not (Test-IngestSection 'defender/serverVulnerabilityAssessmentsSettings')) { return New-SubscriptionFinding (New-Unknown 'The vulnerability assessment setting could not be read and the plan does not report a sub plan' $evidence) }
        $setting = @(Get-IngestData 'defender/serverVulnerabilityAssessmentsSettings') | Where-Object { $_ } | Select-Object -First 1
        $evidence.selectedProvider = $setting.properties.selectedProvider
        if ($evidence.selectedProvider) { return New-SubscriptionFinding (New-Pass "Vulnerability assessment provider $($evidence.selectedProvider)" $evidence) }
        New-SubscriptionFinding (New-Fail 'No vulnerability assessment provider is selected' $evidence)
    }
}

function Get-SecurityContact {
    return @(Get-IngestData 'defender/securityContacts') | Where-Object { $_ } | Select-Object -First 1
}

Add-AzTest @{
    Id          = 'AZ-DEF-018'
    Title       = 'A security contact email address is configured'
    Category    = 'Incident response'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'Medium'
    Description = 'Checks that Defender for Cloud email notifications have at least one additional email address.'
    Rationale   = 'Microsoft and Defender for Cloud use the security contact to report compromised resources and high severity alerts. Without it, notifications may reach nobody who acts on them.'
    Remediation = "Set 'Additional email addresses' (a monitored security team mailbox) under Defender for Cloud > Environment settings > Email notifications."
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/configure-email-notifications')
    Frameworks  = @{ MCSB = 'IR-2'; CIS = '8.1.13'; ALZ = 'Deploy-MDFC-Config-H224' }
    Requires    = @('defender/securityContacts')
    Run         = {
        $contact = Get-SecurityContact
        $emails = @(([string]$contact.properties.emails) -split '[;,]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        $evidence = [ordered]@{ emails = $emails; isEnabled = $contact.properties.isEnabled }
        if ($emails) { return New-SubscriptionFinding (New-Pass "$($emails.Count) security contact address(es)" $evidence) }
        New-SubscriptionFinding (New-Fail 'No security contact email address' $evidence)
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-019'
    Title       = 'Security alert notifications are sent to subscription owners'
    Category    = 'Incident response'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'Low'
    Description = "Checks that Defender for Cloud notifies users with the Owner role about alerts."
    Rationale   = 'Owners are accountable for the subscription and must know when its resources are attacked, especially when no central SOC monitors the alerts.'
    Remediation = "Under Email notifications, select 'Owner' in 'All users with the following roles'."
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/configure-email-notifications')
    Frameworks  = @{ MCSB = 'IR-2'; CIS = '8.1.12' }
    Policy      = @{ '0b15565f-aa9e-48ba-8619-45960f2c314d' = 'Email notification to subscription owner for high severity alerts should be enabled' }
    Requires    = @('defender/securityContacts')
    Run         = {
        $byRole = (Get-SecurityContact).properties.notificationsByRole
        $evidence = [ordered]@{ state = $byRole.state; roles = @($byRole.roles) }
        if ($byRole.state -eq 'On' -and @($byRole.roles) -contains 'Owner') { return New-SubscriptionFinding (New-Pass 'Owners are notified' $evidence) }
        New-SubscriptionFinding (New-Fail 'Owners are not notified about alerts' $evidence)
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-020'
    Title       = 'Email notifications for security alerts are enabled'
    Category    = 'Incident response'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'Medium'
    Description = 'Checks that Defender for Cloud sends email notifications for alerts with a minimum severity of High or lower.'
    Rationale   = 'Alert emails are the minimum notification path so that detected attacks are acted upon quickly.'
    Remediation = "Under Email notifications, enable 'Notify about alerts with the following severity (or higher)' and select High (or Medium)."
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/configure-email-notifications')
    Frameworks  = @{ MCSB = 'IR-2'; CIS = '8.1.14' }
    Policy      = @{ '6e2593d9-add6-4083-9c9b-4b7d2188c899' = 'Email notification for high severity alerts should be enabled' }
    Requires    = @('defender/securityContacts')
    Run         = {
        $contact = Get-SecurityContact
        $source = @($contact.properties.notificationsSources) | Where-Object { $_ -and $_.sourceType -eq 'Alert' } | Select-Object -First 1
        $evidence = [ordered]@{ isEnabled = $contact.properties.isEnabled; minimalSeverity = $source.minimalSeverity }
        if ($contact.properties.isEnabled -and $source.minimalSeverity -in 'High', 'Medium', 'Low') { return New-SubscriptionFinding (New-Pass "Alert emails for severity $($source.minimalSeverity) and higher" $evidence) }
        New-SubscriptionFinding (New-Fail 'Alert email notifications are not enabled' $evidence)
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-021'
    Title       = 'Email notifications for attack paths are enabled'
    Category    = 'Incident response'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'Low'
    Description = 'Checks that Defender for Cloud sends email notifications for attack paths with a minimum risk level.'
    Rationale   = 'Attack paths show exploitable chains from the internet to critical resources. Notifications make sure new high risk paths are handled promptly.'
    Remediation = "Under Email notifications, enable 'Notify about attack paths with the following risk level (or higher)' (requires Defender CSPM)."
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/configure-email-notifications')
    Frameworks  = @{ MCSB = 'IR-2'; CIS = '8.1.15' }
    Requires    = @('defender/securityContacts')
    Run         = {
        $contact = Get-SecurityContact
        $source = @($contact.properties.notificationsSources) | Where-Object { $_ -and $_.sourceType -eq 'AttackPath' } | Select-Object -First 1
        $evidence = [ordered]@{ isEnabled = $contact.properties.isEnabled; minimalRiskLevel = $source.minimalRiskLevel }
        if ($contact.properties.isEnabled -and $source.minimalRiskLevel) { return New-SubscriptionFinding (New-Pass "Attack path emails for risk level $($source.minimalRiskLevel) and higher" $evidence) }
        New-SubscriptionFinding (New-Fail 'Attack path notifications are not enabled' $evidence)
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-022'
    Title       = 'No active high severity security alerts'
    Category    = 'Incident response'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'High'
    Description = 'Lists Defender for Cloud alerts with severity High that are still active (not resolved or dismissed).'
    Rationale   = 'An active high severity alert may be an ongoing compromise. Alerts must be triaged, investigated and closed.'
    Remediation = 'Investigate each alert (Defender for Cloud > Security alerts, or the Defender portal incidents), contain and remediate, then resolve or dismiss it with a reason.'
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/managing-and-responding-alerts')
    Frameworks  = @{ MCSB = @('IR-3', 'IR-4'); WAF = 'SE:12' }
    Requires    = @('defender/alerts')
    Run         = {
        $alerts = @(Get-IngestData 'defender/alerts' | Where-Object { $_ -and $_.properties.severity -eq 'High' -and $_.properties.status -eq 'Active' })
        if (-not $alerts) { return New-SubscriptionFinding (New-Pass 'No active high severity alerts') }
        foreach ($alert in $alerts) {
            $p = $alert.properties
            $evidence = [ordered]@{ alert = $p.alertDisplayName; startTime = Format-UtcDate $p.startTimeUtc; compromisedEntity = $p.compromisedEntity; resource = @($p.resourceIdentifiers | ForEach-Object { $_.azureResourceId } | Where-Object { $_ }) }
            New-Finding -ResourceId $alert.id -ResourceType $alert.type -ResourceName $p.alertDisplayName -Result (New-Fail "$($p.alertDisplayName) on $($p.compromisedEntity) since $($evidence.startTime)" $evidence)
        }
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-024'
    Title       = 'Sensitive data discovery is enabled'
    Category    = 'Data protection'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'Medium'
    Description = 'Checks the sensitive data discovery extension of Defender CSPM or Defender for Storage.'
    Rationale   = 'Knowing where sensitive data lives drives the prioritization of attack paths, alerts and protection; sensitive data discovery classifies data in storage and databases automatically.'
    Remediation = "Enable 'Sensitive data discovery' in the Defender CSPM or Defender for Storage plan settings."
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/concept-data-security-posture')
    Frameworks  = @{ MCSB = 'DP-1'; WAF = 'SE:03' }
    Requires    = @('defender/pricings')
    Run         = {
        $enabledIn = @('CloudPosture', 'StorageAccounts' | Where-Object {
                $pricing = Get-DefenderPricing $_
                $extension = Get-PricingExtension $pricing 'SensitiveDataDiscovery'
                $pricing.properties.pricingTier -eq 'Standard' -and $extension.isEnabled -eq 'True'
            })
        $evidence = [ordered]@{ enabledIn = $enabledIn }
        if ($enabledIn) { return New-SubscriptionFinding (New-Pass "Sensitive data discovery on ($($enabledIn -join ', '))" $evidence) }
        New-SubscriptionFinding (New-Fail 'Sensitive data discovery is off' $evidence)
    }
}

Add-AzTest @{
    Id            = 'AZ-DEF-025'
    Version       = 2
    Title         = 'Defender for Storage malware scanning is enabled'
    Category      = 'Data protection'
    Service       = 'Microsoft Defender for Cloud'
    Severity      = 'Medium'
    Description   = 'Checks that on-upload malware scanning of Defender for Storage applies to each storage account (plan extension or account level override).'
    Rationale     = 'Storage accounts that receive files from users or partners are a malware distribution path; scanning on upload detects malicious content before it is consumed.'
    Remediation   = "Enable 'Malware scanning' in the Defender for Storage plan settings (with a monthly cap per account), or on the individual storage account."
    References    = @('https://learn.microsoft.com/azure/defender-for-cloud/on-upload-malware-scanning')
    Frameworks    = @{ MCSB = @('DP-2', 'ES-2') }
    Requires      = @('defender/pricings')
    ResourceTypes = @('Microsoft.Storage/storageAccounts')
    Evaluate      = {
        param($Record)
        if (-not (Test-ChildCollected $Record 'providers/Microsoft.Security/defenderForStorageSettings/current')) { return New-Unknown 'The Defender for Storage setting of the account could not be read' }
        $setting = (Get-Child $Record 'providers/Microsoft.Security/defenderForStorageSettings/current').properties
        $pricing = Get-DefenderPricing 'StorageAccounts'
        $planScanning = $pricing.properties.pricingTier -eq 'Standard' -and (Get-PricingExtension $pricing 'OnUploadMalwareScanning').isEnabled -eq 'True'
        $evidence = [ordered]@{ planMalwareScanning = $planScanning; accountOverride = [bool]$setting.overrideSubscriptionLevelSettings; accountMalwareScanning = $setting.malwareScanning.onUpload.isEnabled }
        $enabled = if ($setting.overrideSubscriptionLevelSettings) { $setting.isEnabled -and $setting.malwareScanning.onUpload.isEnabled } else { $planScanning }
        if ($enabled) { return New-Pass 'Malware scanning on upload enabled' $evidence }
        New-Fail 'No malware scanning on upload' $evidence
    }
}

Add-AzTest @{
    Id          = 'AZ-DEF-023'
    Version     = 2
    Title       = 'Security alerts are forwarded to a SIEM or automation'
    Category    = 'Incident response'
    Service     = 'Microsoft Defender for Cloud'
    Severity    = 'Low'
    Description = 'Checks for a Defender for Cloud workflow automation or continuous export that handles security alerts, or the Microsoft Sentinel alert synchronization setting.'
    Rationale   = 'Alerts that stay in the portal depend on someone looking. Forwarding them to a SIEM, SOAR or ticketing flow makes sure every alert is triaged.'
    Remediation = 'Connect Defender for Cloud to Microsoft Sentinel (or your SIEM) through the Defender XDR connector or continuous export, or create a workflow automation for alerts.'
    References  = @('https://learn.microsoft.com/azure/defender-for-cloud/continuous-export')
    Frameworks  = @{ MCSB = @('IR-2', 'LT-5'); WAF = 'SE:10' }
    Requires    = @('defender/automations', 'defender/settings')
    Run         = {
        $automations = @(Get-IngestData 'defender/automations' | Where-Object { $_ -and $_.properties.isEnabled -and (@($_.properties.sources) | Where-Object { $_.eventSource -eq 'Alerts' }) })
        $sentinel = @(Get-IngestData 'defender/settings') | Where-Object { $_ -and $_.name -eq 'Sentinel' -and $_.properties.enabled }
        $evidence = [ordered]@{ alertAutomations = @($automations | ForEach-Object name | Sort-Object); sentinelAlertSync = [bool]$sentinel }
        if ($automations -or $sentinel) { return New-SubscriptionFinding (New-Pass 'Alerts are forwarded' $evidence) }
        New-SubscriptionFinding (New-Fail 'No alert export, automation or Sentinel synchronization configured for this subscription' $evidence)
    }
}