Private/M365Monitor/Core/Get-M365AuditEvents.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 Get-M365AuditEvents { <# .SYNOPSIS Collects M365 audit events from Microsoft Graph for security monitoring. .DESCRIPTION Uses Invoke-GraphApi to fetch directory audit logs, sign-in logs, and security alerts filtered by M365 service categories including Exchange, SharePoint/OneDrive, Teams, Defender, and Power Platform. Fast mode: Exchange transport/forwarding rules + audit log status changes only. Full mode: All M365 service categories. .PARAMETER AccessToken Microsoft Graph access token. .PARAMETER StartTime Start time for event collection window. .PARAMETER ScanMode Fast or Full scan mode. .PARAMETER Quiet Suppress progress output. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$AccessToken, [Parameter(Mandatory)] [datetime]$StartTime, [ValidateSet('Fast', 'Full')] [string]$ScanMode = 'Fast', [switch]$Quiet ) $result = @{ ExchangeTransportRules = [System.Collections.Generic.List[PSCustomObject]]::new() ExchangeForwardingRules = [System.Collections.Generic.List[PSCustomObject]]::new() EDiscoverySearches = [System.Collections.Generic.List[PSCustomObject]]::new() DLPPolicyChanges = [System.Collections.Generic.List[PSCustomObject]]::new() SharePointSharingChanges = [System.Collections.Generic.List[PSCustomObject]]::new() SharePointFileOperations = [System.Collections.Generic.List[PSCustomObject]]::new() TeamsAccessChanges = [System.Collections.Generic.List[PSCustomObject]]::new() DefenderAlertChanges = [System.Collections.Generic.List[PSCustomObject]]::new() PowerPlatformFlows = [System.Collections.Generic.List[PSCustomObject]]::new() AuditLogChanges = [System.Collections.Generic.List[PSCustomObject]]::new() SecurityAlerts = [System.Collections.Generic.List[PSCustomObject]]::new() Errors = @{} } $filterDate = $StartTime.ToString('yyyy-MM-ddTHH:mm:ssZ') # ── Exchange Transport Rule Changes ────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Exchange transport rules' } try { $transportRuleActivities = @( 'New-TransportRule' 'Set-TransportRule' 'Remove-TransportRule' 'Enable-TransportRule' 'Disable-TransportRule' ) $auditLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate and loggedByService eq 'Exchange'" '$top' = '999' '$orderby' = 'activityDateTime desc' } ` -Paginate -Quiet:$Quiet if ($auditLogs) { foreach ($log in $auditLogs) { $activity = $log.activityDisplayName $operationName = $log.operationType ?? $activity # Transport rule operations if ($activity -match 'TransportRule|transport rule|mail flow rule' -or $operationName -in $transportRuleActivities) { $result.ExchangeTransportRules.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $activity OperationType = $operationName Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' TargetId = ($log.targetResources | Select-Object -First 1).id ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } # Forwarding rule operations if ($activity -match 'forwarding|inbox rule|Set-Mailbox.*Forward|redirect' -or $operationName -match 'New-InboxRule|Set-InboxRule|Set-Mailbox') { $result.ExchangeForwardingRules.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $activity OperationType = $operationName Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' TargetId = ($log.targetResources | Select-Object -First 1).id ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } # Audit log status changes if ($activity -match 'audit|unified audit|AdminAuditLog' -or $operationName -match 'Set-AdminAuditLogConfig|Set-OrganizationConfig.*AuditDisabled') { $result.AuditLogChanges.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $activity OperationType = $operationName Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Exchange transport rules' ` -Detail "$($result.ExchangeTransportRules.Count) transport, $($result.ExchangeForwardingRules.Count) forwarding, $($result.AuditLogChanges.Count) audit config" } } catch { $result.Errors['ExchangeAudit'] = $_.Exception.Message Write-Verbose "Exchange audit log fetch failed: $_" } # ── Fast mode stops here with just Exchange + audit log ────────────── if ($ScanMode -eq 'Full') { # ── Exchange Mailbox Forwarding (via management activity API patterns) ── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Mailbox forwarding rules (server-side)' } try { # Query for mailbox configuration changes that set forwarding $mailboxLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate and loggedByService eq 'Exchange' and activityDisplayName eq 'Set-Mailbox'" '$top' = '500' } ` -Paginate -Quiet:$Quiet if ($mailboxLogs) { foreach ($log in $mailboxLogs) { $modProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | Where-Object { $_.displayName -match 'ForwardingSmtpAddress|ForwardingAddress|DeliverToMailboxAndForward' } }) if ($modProps.Count -gt 0) { $result.ExchangeForwardingRules.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = 'Set-Mailbox (Forwarding)' OperationType = 'Set-Mailbox' Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' TargetId = ($log.targetResources | Select-Object -First 1).id ?? '' ModifiedProps = @($modProps | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } }) RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Mailbox forwarding' -Detail "$($result.ExchangeForwardingRules.Count) total" } } catch { $result.Errors['MailboxForwarding'] = $_.Exception.Message Write-Verbose "Mailbox forwarding fetch failed: $_" } # ── eDiscovery / Compliance Search ─────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'eDiscovery compliance searches' } try { $complianceLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate and loggedByService eq 'Core Directory'" '$top' = '500' } ` -Paginate -Quiet:$Quiet if ($complianceLogs) { foreach ($log in $complianceLogs) { if ($log.activityDisplayName -match 'eDiscovery|ComplianceSearch|content search|SearchCreated|SearchStarted') { $result.EDiscoverySearches.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $log.activityDisplayName OperationType = $log.operationType ?? '' Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'eDiscovery searches' -Detail "$($result.EDiscoverySearches.Count) found" } } catch { $result.Errors['eDiscovery'] = $_.Exception.Message Write-Verbose "eDiscovery search fetch failed: $_" } # ── DLP Policy Changes ─────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'DLP policy changes' } try { $dlpLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate" '$top' = '500' } ` -Paginate -Quiet:$Quiet if ($dlpLogs) { foreach ($log in $dlpLogs) { if ($log.activityDisplayName -match 'DLP|DataLossPrevent|DlpPolicy|DlpRule|DlpCompliancePolicy') { $result.DLPPolicyChanges.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $log.activityDisplayName OperationType = $log.operationType ?? '' Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'DLP policy changes' -Detail "$($result.DLPPolicyChanges.Count) found" } } catch { $result.Errors['DLPPolicy'] = $_.Exception.Message Write-Verbose "DLP policy fetch failed: $_" } # ── SharePoint External Sharing Changes ────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'SharePoint external sharing changes' } try { $spLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate and loggedByService eq 'SharePoint'" '$top' = '999' } ` -Paginate -Quiet:$Quiet if ($spLogs) { foreach ($log in $spLogs) { $activity = $log.activityDisplayName # Sharing policy changes if ($activity -match 'sharing|SharingPolicy|external.*access|anonymous.*link|guest.*access|SharingCapability') { $result.SharePointSharingChanges.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $activity OperationType = $log.operationType ?? '' Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } # Bulk file operations (download, copy, move, sync) if ($activity -match 'FileDownloaded|FilePreviewed|FileModified|FileSyncDownload|FileAccessed|FileCopied|FileMoved') { $result.SharePointFileOperations.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $activity TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' TargetId = ($log.targetResources | Select-Object -First 1).id ?? '' RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'SharePoint changes' ` -Detail "$($result.SharePointSharingChanges.Count) sharing, $($result.SharePointFileOperations.Count) file ops" } } catch { $result.Errors['SharePoint'] = $_.Exception.Message Write-Verbose "SharePoint audit fetch failed: $_" } # ── Teams External Access Changes ──────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Teams external access changes' } try { $teamsLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate and loggedByService eq 'Teams'" '$top' = '500' } ` -Paginate -Quiet:$Quiet if ($teamsLogs) { foreach ($log in $teamsLogs) { if ($log.activityDisplayName -match 'external.*access|guest.*access|federation|AllowedDomains|BlockedDomains|TeamsGuestAccess|TeamsExternalAccess|TeamsMeetingPolicy') { $result.TeamsAccessChanges.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $log.activityDisplayName OperationType = $log.operationType ?? '' Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Teams access changes' -Detail "$($result.TeamsAccessChanges.Count) found" } } catch { $result.Errors['TeamsAccess'] = $_.Exception.Message Write-Verbose "Teams access fetch failed: $_" } # ── Defender Alert Policy Changes ──────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Defender alert policy changes' } try { # Fetch audit logs related to Defender/Security policy changes $defenderLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate" '$top' = '500' } ` -Paginate -Quiet:$Quiet if ($defenderLogs) { foreach ($log in $defenderLogs) { if ($log.activityDisplayName -match 'AlertPolicy|alert.*policy|ProtectionAlert|threat.*policy|SafeAttach|SafeLink|AntiPhish|Defender') { $result.DefenderAlertChanges.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $log.activityDisplayName OperationType = $log.operationType ?? '' Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Defender alert changes' -Detail "$($result.DefenderAlertChanges.Count) found" } } catch { $result.Errors['DefenderAlerts'] = $_.Exception.Message Write-Verbose "Defender alert fetch failed: $_" } # ── Power Platform / Power Automate Flows ──────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Power Automate flow creation' } try { $powerLogs = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/auditLogs/directoryAudits' ` -QueryParameters @{ '$filter' = "activityDateTime ge $filterDate and loggedByService eq 'Power Platform'" '$top' = '500' } ` -Paginate -Quiet:$Quiet if ($powerLogs) { foreach ($log in $powerLogs) { if ($log.activityDisplayName -match 'CreateFlow|EditFlow|flow.*created|flow.*modified|Power.*Automate|LogicApp') { $result.PowerPlatformFlows.Add([PSCustomObject]@{ Timestamp = $log.activityDateTime Actor = $log.initiatedBy.user.userPrincipalName ?? $log.initiatedBy.app.displayName ?? 'Unknown' ActorId = $log.initiatedBy.user.id ?? $log.initiatedBy.app.appId ?? '' Activity = $log.activityDisplayName OperationType = $log.operationType ?? '' Result = $log.result TargetName = ($log.targetResources | Select-Object -First 1).displayName ?? '' ModifiedProps = @($log.targetResources | ForEach-Object { $_.modifiedProperties | ForEach-Object { @{ Name = $_.displayName; OldValue = $_.oldValue; NewValue = $_.newValue } } }) RawLog = $log }) } } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Power Automate flows' -Detail "$($result.PowerPlatformFlows.Count) found" } } catch { $result.Errors['PowerPlatform'] = $_.Exception.Message Write-Verbose "Power Platform fetch failed: $_" } # ── Security Alerts (Defender alerts_v2) ───────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Security alerts (Defender)' } try { $alerts = Invoke-GraphApi -AccessToken $AccessToken ` -Uri '/security/alerts_v2' ` -QueryParameters @{ '$filter' = "createdDateTime ge $filterDate" '$top' = '999' '$orderby' = 'createdDateTime desc' } ` -Paginate -Quiet:$Quiet if ($alerts) { foreach ($alert in $alerts) { $result.SecurityAlerts.Add([PSCustomObject]@{ Timestamp = $alert.createdDateTime AlertId = $alert.id Title = $alert.title Description = $alert.description Severity = $alert.severity Status = $alert.status Category = $alert.category Source = $alert.detectionSource ?? $alert.serviceSource ?? '' ThreatName = $alert.threatDisplayName ?? '' UserStates = @($alert.evidence | Where-Object { $_.'@odata.type' -match 'user' } | ForEach-Object { @{ UserPrincipalName = $_.userAccount.accountName ?? '' DomainName = $_.userAccount.domainName ?? '' } }) RawAlert = $alert }) } } if (-not $Quiet) { Write-ProgressLine -Phase SCANNING -Message 'Security alerts' -Detail "$($result.SecurityAlerts.Count) found" } } catch { $result.Errors['SecurityAlerts'] = $_.Exception.Message Write-Verbose "Security alerts fetch failed: $_" } } return $result } |