Private/M365Monitor/Detections/Test-M365AuditLogDisablement.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 Test-M365AuditLogDisablement {
    [CmdletBinding()]
    param(
        [PSCustomObject[]]$Events = @()
    )

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Operations that directly disable or reduce audit logging
    $disableOperations = @(
        'Set-AdminAuditLogConfig'
        'Disable-OrganizationCustomization'
        'Set-OrganizationConfig'
        'Set-Mailbox'
        'Set-MailboxAuditBypassAssociation'
        'Disable-Mailbox'
    )

    # Property patterns that indicate audit weakening
    $auditDisableProperties = @(
        'UnifiedAuditLogIngestionEnabled'
        'AuditDisabled'
        'AuditEnabled'
        'AuditLogAgeLimit'
        'AdminAuditLogEnabled'
        'AdminAuditLogCmdlets'
        'AdminAuditLogParameters'
        'AdminAuditLogAgeLimit'
        'MailboxAuditLogEnabled'
        'AuditAdmin'
        'AuditDelegate'
        'AuditOwner'
        'AuditBypassEnabled'
    )

    foreach ($event in $Events) {
        $activity = $event.Activity ?? ''
        $operationType = $event.OperationType ?? $activity
        $targetName = $event.TargetName ?? ''
        $auditDisabled = $false
        $auditReduced = $false
        $bypassAdded = $false
        $affectedScope = 'unknown'
        $changeDetails = [System.Collections.Generic.List[string]]::new()

        foreach ($prop in $event.ModifiedProps) {
            $propName = $prop.Name ?? ''
            $newVal = $prop.NewValue ?? ''
            $oldVal = $prop.OldValue ?? ''
            $cleanNew = ($newVal -replace '"', '').Trim()
            $cleanOld = ($oldVal -replace '"', '').Trim()

            # Unified audit log disabled at organization level
            if ($propName -eq 'UnifiedAuditLogIngestionEnabled') {
                if ($cleanNew -match 'false|False|0') {
                    $auditDisabled = $true
                    $affectedScope = 'Organization'
                    $changeDetails.Add('CRITICAL: Unified Audit Log ingestion DISABLED for entire organization')
                }
            }

            # AuditDisabled flag set to true (inverse logic: true = auditing off)
            if ($propName -eq 'AuditDisabled') {
                if ($cleanNew -match 'true|True|1') {
                    $auditDisabled = $true
                    $affectedScope = 'Organization'
                    $changeDetails.Add('CRITICAL: Organization audit logging DISABLED')
                }
            }

            # AuditEnabled flag set to false (per-mailbox)
            if ($propName -eq 'AuditEnabled') {
                if ($cleanNew -match 'false|False|0' -and $cleanOld -match 'true|True|1') {
                    $auditDisabled = $true
                    $affectedScope = "Mailbox: $targetName"
                    $changeDetails.Add("Mailbox audit logging DISABLED for: $targetName")
                }
            }

            # AdminAuditLogEnabled = False
            if ($propName -eq 'AdminAuditLogEnabled') {
                if ($cleanNew -match 'false|False|0') {
                    $auditDisabled = $true
                    $affectedScope = 'Admin Audit'
                    $changeDetails.Add('CRITICAL: Admin audit logging DISABLED')
                }
            }

            # MailboxAuditLogEnabled = False
            if ($propName -eq 'MailboxAuditLogEnabled') {
                if ($cleanNew -match 'false|False|0') {
                    $auditDisabled = $true
                    $affectedScope = "Mailbox: $targetName"
                    $changeDetails.Add("Mailbox audit log DISABLED for: $targetName")
                }
            }

            # Audit log age limit reduced (anti-forensics)
            if ($propName -match 'AuditLogAgeLimit|AdminAuditLogAgeLimit') {
                try {
                    $newDays = if ($cleanNew -match '^(\d+)\.') { [int]$Matches[1] }
                               elseif ($cleanNew -match '^\d+$') { [int]$cleanNew }
                               else { -1 }
                    $oldDays = if ($cleanOld -match '^(\d+)\.') { [int]$Matches[1] }
                               elseif ($cleanOld -match '^\d+$') { [int]$cleanOld }
                               else { -1 }
                    if ($newDays -ge 0 -and $oldDays -gt 0 -and $newDays -lt $oldDays) {
                        $auditReduced = $true
                        $changeDetails.Add("Audit retention reduced from $oldDays to $newDays days")
                    } else {
                        $changeDetails.Add("Audit log age limit changed: '$cleanOld' -> '$cleanNew'")
                    }
                } catch {
                    $changeDetails.Add("Audit log age limit changed: '$cleanOld' -> '$cleanNew'")
                }
            }

            # Audit scope reduction (fewer cmdlets/parameters audited)
            if ($propName -match 'AdminAuditLogCmdlets|AdminAuditLogParameters|AdminAuditLogExcludedCmdlets') {
                if ($cleanOld -eq '*' -and $cleanNew -ne '*') {
                    $auditReduced = $true
                    $changeDetails.Add("Audit scope narrowed: $propName changed from wildcard to '$cleanNew'")
                } elseif (-not $cleanNew -or $cleanNew -eq '' -or $cleanNew -eq '""') {
                    $auditReduced = $true
                    $changeDetails.Add("Audit scope cleared: $propName was '$cleanOld'")
                } else {
                    $changeDetails.Add("Admin audit log filter modified: $propName changed")
                }
            }

            # Mailbox audit actions reduced
            if ($propName -match '^AuditAdmin$|^AuditDelegate$|^AuditOwner$') {
                if ($cleanOld -and $cleanNew) {
                    $oldActions = @($cleanOld -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
                    $newActions = @($cleanNew -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
                    if ($newActions.Count -lt $oldActions.Count) {
                        $auditReduced = $true
                        $removed = @($oldActions | Where-Object { $_ -notin $newActions })
                        $changeDetails.Add("Audit actions reduced on $propName, removed: $($removed -join ', ')")
                    }
                } elseif ($cleanOld -and (-not $cleanNew -or $cleanNew -eq '')) {
                    $auditReduced = $true
                    $changeDetails.Add("All $propName audit actions removed (was: $cleanOld)")
                }
            }

            # Audit bypass association
            if ($propName -match 'AuditBypassEnabled|BypassAudit|BypassEnabled') {
                if ($cleanNew -match 'true|True|1') {
                    $bypassAdded = $true
                    $changeDetails.Add("Audit bypass enabled for mailbox: $targetName")
                }
            }
        }

        # Check the operation name if no property-level match
        if ($operationType -match 'Set-MailboxAuditBypassAssociation') {
            $bypassAdded = $true
            if ($changeDetails.Count -eq 0) {
                $changeDetails.Add("Mailbox audit bypass association modified for: $targetName")
            }
        }

        # Also check activity name directly for audit disable signals
        if (-not $auditDisabled -and -not $auditReduced -and -not $bypassAdded) {
            if ($activity -match 'DisableAudit|disable.*audit|AuditDisabled|UnifiedAuditLog.*disable') {
                $auditDisabled = $true
                $affectedScope = 'Organization'
                $changeDetails.Add("Audit logging disabled via: $activity")
            }
        }

        # Only report if there is an actual audit-related change
        if (-not $auditDisabled -and -not $auditReduced -and -not $bypassAdded -and $changeDetails.Count -eq 0) {
            $isAuditRelated = $false
            foreach ($auditProp in $auditDisableProperties) {
                foreach ($prop in $event.ModifiedProps) {
                    if (($prop.Name ?? '') -match $auditProp) {
                        $isAuditRelated = $true
                        break
                    }
                }
                if ($isAuditRelated) { break }
            }

            if (-not $isAuditRelated -and $activity -notmatch 'audit|AdminAuditLog') {
                continue
            }

            $changeDetails.Add("Audit configuration modified: $activity")
        }

        # Severity assessment -- audit log disablement is always critical or high
        $severity = if ($auditDisabled) { 'Critical' }
                    elseif ($bypassAdded) { 'Critical' }
                    elseif ($auditReduced) { 'High' }
                    else { 'High' }

        $description = if ($auditDisabled) {
            "CRITICAL: Audit logging DISABLED ($affectedScope) by $($event.Actor)"
        } elseif ($bypassAdded) {
            "AUDIT BYPASS ADDED: '$targetName' exempted from audit logging by $($event.Actor)"
        } elseif ($auditReduced) {
            "AUDIT SCOPE REDUCED: $operationType on '$targetName' by $($event.Actor)"
        } else {
            "Audit log configuration changed: $operationType on '$targetName' by $($event.Actor)"
        }

        $results.Add([PSCustomObject]@{
            Timestamp     = $event.Timestamp
            Actor         = $event.Actor
            DetectionType = 'm365AuditLogDisablement'
            Description   = $description
            Details       = @{
                OperationType = $operationType
                TargetName    = $targetName
                AuditDisabled = $auditDisabled
                AuditReduced  = $auditReduced
                BypassAdded   = $bypassAdded
                AffectedScope = $affectedScope
                ChangeNotes   = @($changeDetails)
                ModifiedProps = $event.ModifiedProps
            }
            Severity      = $severity
        })
    }

    return @($results)
}