Lesson 1 of 3 · 25 min read

M365 Incident Response — Unified Audit Log and Email Compromise

M365 IR differs from endpoint IR in one fundamental way: you are investigating API calls and log entries rather than disk artifacts. The good news is that cloud audit logs are centralized, timestamped by the cloud provider, and not susceptible to T1070 (log clearing) by a compromised endpoint.


Initial M365 Investigation Workflow

# Connect to Exchange Online (requires ExchangeOnlineManagement module)
Connect-ExchangeOnline -UserPrincipalName admin@company.com

# Search UAL for compromised user activity (last 90 days)
$results = Search-UnifiedAuditLog `
  -UserIds "jsmith@company.com" `
  -StartDate (Get-Date).AddDays(-30) `
  -EndDate (Get-Date) `
  -ResultSize 5000

# Export to CSV for analysis
$results | Select-Object CreationDate, UserIds, Operations, `
  @{N='AuditData';E={$_.AuditData | ConvertFrom-Json | ConvertTo-Json -Depth 5}} |
  Export-Csv -Path "jsmith_audit.csv" -NoTypeInformation

# Key operations to search for:
$critical_ops = @(
  "UserLoggedIn",          # All login events with IP
  "New-InboxRule",         # Email forwarding/deletion rules
  "Send",                  # Emails sent
  "FileDownloaded",        # OneDrive downloads
  "FileAccessed",          # SharePoint access
  "ConsentToApplication",  # OAuth app consent
  "Add member to role",    # Admin escalation
  "PasswordReset"          # Account password changes
)

Search-UnifiedAuditLog `
  -UserIds "jsmith@company.com" `
  -Operations $critical_ops `
  -StartDate (Get-Date).AddDays(-30) `
  -EndDate (Get-Date) `
  -ResultSize 5000

BEC Investigation — Email Forwarding Rules

# Find all inbox rules for the compromised user
Get-InboxRule -Mailbox "jsmith@company.com" |
  Select-Object Name, Description, ForwardTo, DeleteMessage, `
  MarkAsRead, MoveToFolder, Conditions |
  Format-List

# Search UAL for when the forwarding rule was created
$rule_events = Search-UnifiedAuditLog `
  -Operations "New-InboxRule" `
  -UserIds "jsmith@company.com" `
  -StartDate (Get-Date).AddDays(-90) `
  -EndDate (Get-Date)

$rule_events | ForEach-Object {
  $data = $_.AuditData | ConvertFrom-Json
  [PSCustomObject]@{
    Time = $_.CreationDate
    User = $_.UserIds
    ClientIP = $data.ClientIP
    Parameters = $data.Parameters | ConvertTo-Json
  }
}

# Find all emails forwarded to the attacker's address
Get-MessageTrace `
  -SenderAddress "jsmith@company.com" `
  -StartDate (Get-Date).AddDays(-30) `
  -EndDate (Get-Date) |
  Where-Object {$_.RecipientAddress -notlike "*@company.com"} |
  Select-Object Received, SenderAddress, RecipientAddress, Subject, Status

# Check for login location anomaly (different IP for login vs. file access)
$logins = Search-UnifiedAuditLog `
  -Operations "UserLoggedIn" `
  -UserIds "jsmith@company.com" `
  -StartDate (Get-Date).AddDays(-7) `
  -EndDate (Get-Date)

$file_access = Search-UnifiedAuditLog `
  -Operations "FileAccessed", "FileDownloaded" `
  -UserIds "jsmith@company.com" `
  -StartDate (Get-Date).AddDays(-7) `
  -EndDate (Get-Date)

# Compare IPs between login and file access
# Login from 203.x.x.x (user's home IP) but FileAccessed from 185.x.x.x = session theft
$logins | ForEach-Object {
  $data = $_.AuditData | ConvertFrom-Json
  Write-Host "Login: $($_.CreationDate) | IP: $($data.ClientIP) | MFA: $($data.DeviceProperties)"
}

M365 Containment Actions

# Step 1: Invalidate ALL current sessions and tokens
Revoke-MgUserSignInSession -UserId "jsmith@company.com"
# This forces re-authentication on all devices — kills stolen session cookies

# Step 2: Reset the password (do AFTER revoking sessions — order matters)
$newPwd = [System.Web.Security.Membership]::GeneratePassword(20, 5)
Update-MgUser -UserId "jsmith@company.com" `
  -PasswordProfile @{Password=$newPwd; ForceChangePasswordNextSignIn=$true}

# Step 3: Remove malicious inbox rules
Get-InboxRule -Mailbox "jsmith@company.com" | 
  Where-Object {$_.ForwardTo -ne $null -or $_.DeleteMessage -eq $true} |
  Remove-InboxRule -Confirm:$false

# Step 4: Remove unauthorized OAuth app consents
# Azure Portal → Enterprise Applications → Filter by user → Remove
# Or PowerShell:
$app = Get-MgServicePrincipal -Filter "DisplayName eq 'MaliciousApp'"
Remove-MgUserAppRoleAssignment -UserId "jsmith@company.com" `
  -AppRoleAssignmentId $app.Id

# Step 5: Enable MFA and block legacy authentication
# Conditional Access policy — block all legacy auth protocols