Private/Entra/Checks/Invoke-M365DefenderChecks.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Invoke-M365DefenderChecks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$AuditData
    )

    $checkDefs = Get-AuditCategoryDefinitions -Category 'M365DefenderChecks'
    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($check in $checkDefs.checks) {
        $funcName = "Test-Infiltration$($check.id -replace '-', '')"
        if (Get-Command $funcName -ErrorAction SilentlyContinue) {
            try {
                $finding = & $funcName -AuditData $AuditData -CheckDefinition $check
                if ($finding) { $findings.Add($finding) }
            } catch {
                $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' `
                    -CurrentValue "Check failed: $_"))
            }
        } else {
            $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' `
                -CurrentValue 'Check not yet implemented'))
        }
    }

    return @($findings)
}

# ── M365DEF-001: Preset Security Policies ────────────────────────────
function Test-InfiltrationM365DEF001 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $defender = $AuditData.M365Services.Defender
    if (-not $defender -or -not $defender.ProtectionPolicyRules) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Defender for Office 365 protection policy data not available (EXO module not connected or no Defender license)'
    }

    $rules = $defender.ProtectionPolicyRules
    if ($rules.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No preset security policy rules found — consider enabling Standard or Strict preset policies' `
            -Details @{ PresetPolicyCount = 0 }
    }

    # Check for Standard and Strict preset policies
    $standardPreset = @($rules | Where-Object {
        $_.Name -match 'Standard' -or $_.Identity -match 'Standard'
    })
    $strictPreset = @($rules | Where-Object {
        $_.Name -match 'Strict' -or $_.Identity -match 'Strict'
    })

    $enabledRules = @($rules | Where-Object { $_.State -eq 'Enabled' })

    $status = if ($strictPreset.Count -gt 0 -and ($strictPreset | Where-Object { $_.State -eq 'Enabled' })) { 'PASS' }
              elseif ($standardPreset.Count -gt 0 -and ($standardPreset | Where-Object { $_.State -eq 'Enabled' })) { 'PASS' }
              elseif ($standardPreset.Count -gt 0 -or $strictPreset.Count -gt 0) { 'WARN' }
              else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Preset policies: $($standardPreset.Count) Standard, $($strictPreset.Count) Strict ($($enabledRules.Count) of $($rules.Count) rules enabled)" `
        -Details @{
            StandardPresetCount = $standardPreset.Count
            StrictPresetCount = $strictPreset.Count
            TotalRules = $rules.Count
            EnabledRules = $enabledRules.Count
            Rules = @($rules | ForEach-Object {
                @{
                    Name = $_.Name
                    Identity = $_.Identity
                    State = $_.State
                    Priority = $_.Priority
                }
            })
        }
}

# ── M365DEF-002: Alert Policy Inventory ──────────────────────────────
function Test-InfiltrationM365DEF002 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $defender = $AuditData.M365Services.Defender
    if (-not $defender -or -not $defender.ProtectionAlerts) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Defender protection alert data not available'
    }

    $alerts = $defender.ProtectionAlerts
    if ($alerts.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'No alert policies found — default alert policies may have been removed' `
            -Details @{ AlertCount = 0 }
    }

    $enabled = @($alerts | Where-Object { $_.IsEnabled -eq $true -or $_.Disabled -eq $false })
    $disabled = @($alerts | Where-Object { $_.IsEnabled -eq $false -or $_.Disabled -eq $true })

    # Group by severity for reporting
    $highSeverity = @($alerts | Where-Object { $_.Severity -eq 'High' })
    $mediumSeverity = @($alerts | Where-Object { $_.Severity -eq 'Medium' })
    $lowSeverity = @($alerts | Where-Object { $_.Severity -eq 'Low' -or $_.Severity -eq 'Informational' })

    # Check that critical default alerts are enabled
    $disabledHighSeverity = @($disabled | Where-Object { $_.Severity -eq 'High' })

    $status = if ($disabledHighSeverity.Count -gt 0) { 'FAIL' }
              elseif ($disabled.Count -gt 0) { 'WARN' }
              else { 'PASS' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($alerts.Count) alert policies ($($enabled.Count) enabled, $($disabled.Count) disabled). Severity: $($highSeverity.Count) High, $($mediumSeverity.Count) Medium, $($lowSeverity.Count) Low/Info" `
        -Details @{
            TotalAlerts = $alerts.Count
            EnabledCount = $enabled.Count
            DisabledCount = $disabled.Count
            HighSeverityCount = $highSeverity.Count
            MediumSeverityCount = $mediumSeverity.Count
            LowSeverityCount = $lowSeverity.Count
            DisabledHighSeverityCount = $disabledHighSeverity.Count
            Alerts = @($alerts | Select-Object -First 30 | ForEach-Object {
                @{
                    Name = $_.Name
                    Severity = $_.Severity
                    Category = $_.Category
                    IsEnabled = $_.IsEnabled
                }
            })
        }
}

# ── M365DEF-003: Threat Intelligence Configuration ───────────────────
function Test-InfiltrationM365DEF003 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $defender = $AuditData.M365Services.Defender
    if (-not $defender) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Defender for Office 365 data not available'
    }

    # Check for AIR (Automated Investigation and Response) configuration
    $airConfig = $defender.AIRConfiguration
    $threatExplorer = $defender.ThreatExplorerEnabled

    # If we have no specific threat intel data, check what we can from protection rules
    if (-not $airConfig -and -not $threatExplorer -and -not $defender.ProtectionPolicyRules) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Threat intelligence configuration data not available — requires Defender for Office 365 Plan 2'
    }

    $issues = [System.Collections.Generic.List[string]]::new()
    $details = @{}

    # Check AIR configuration if available
    if ($airConfig) {
        $details['AIREnabled'] = $airConfig.Enabled
        if ($airConfig.Enabled -ne $true) {
            $issues.Add('Automated Investigation and Response (AIR) is not enabled')
        }
    } else {
        $details['AIREnabled'] = 'Unknown'
        $issues.Add('AIR configuration data not available — may require Defender P2 license')
    }

    # Check Threat Explorer availability
    if ($null -ne $threatExplorer) {
        $details['ThreatExplorerEnabled'] = $threatExplorer
        if ($threatExplorer -ne $true) {
            $issues.Add('Threat Explorer is not enabled')
        }
    } else {
        $details['ThreatExplorerEnabled'] = 'Unknown'
    }

    # Evaluate based on preset policy rules as a proxy for overall Defender configuration
    if ($defender.ProtectionPolicyRules) {
        $details['ProtectionRuleCount'] = $defender.ProtectionPolicyRules.Count
    }

    $status = if ($issues.Count -eq 0) { 'PASS' }
              elseif ($issues.Count -eq 1 -and $issues[0] -match 'not available') { 'WARN' }
              else { 'WARN' }

    $description = if ($issues.Count -eq 0) {
        'Threat intelligence components (AIR, Threat Explorer) are configured'
    } else {
        "Threat intelligence: $($issues.Count) issue(s) — $($issues -join '; ')"
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue $description `
        -Details $details
}