secskills
secskills / defense / investigating-m365-entra

investigating-m365-entra

defense verified 2026-07-27

Investigate security incidents in Microsoft 365 and Entra ID (Azure AD) -- search the Unified Audit Log, correlate sign-in and audit events, trace illicit OAuth consent grants, analyze mailbox rule manipulation, and contain compromised identities. Use when responding to a BEC incident, investigating Entra ID compromise, analyzing suspicious OAuth app permissions, tracing mail forwarding abuse, or reviewing Azure AD sign-in anomalies.

$ /plugin install secskills-defense $ /plugin install secskills-core

M365/Entra investigations differ from on-premises DFIR -- there are no disk images, no memory dumps, and no event logs you can collect yourself. Everything comes from API queries against Microsoft's log stores, several of which require E5 licensing or advanced audit to retain what you need. Knowing which logs exist, which ones are missing, and how long they last is half the investigation.

When to Use

When NOT to Use

Log Landscape and Retention

Before you query anything, establish what you have and how far back it goes. Missing logs are a finding, not a reason to skip the question.

Log sourceRetention (default)License gateKey operations
Unified Audit Log (UAL)180d default; 1 year for E5-licensed usersMailItemsAccessed requires E5MailItemsAccessed, New-InboxRule, Set-Mailbox, consent grants
Entra ID sign-in logs30dExport to Log Analytics for longerSign-in events, CA evaluation, MFA results
Entra ID audit logs30dNoneRole assignments, app registrations, credential changes
Identity Protection30dRequires P2Risky sign-ins, risk detections
Defender for Cloud Apps180dRequires MDCA licenseActivity log, OAuth app inventory
Mailbox audit log90d (on by default)MailItemsAccessed requires E5Mail access, send-as, delegate ops

Get the retention right before you conclude anything from a gap. Since 17 October 2023 the Audit (Standard) default is 180 days, not the 90 days most older material still cites — records generated before that date kept the old 90-day window. One year is the default only for users holding an E5 (or Purview Audit add-on) licence, and even then only for Exchange, SharePoint, OneDrive, and Entra ID. An E3 tenant therefore has 180 days of UAL and no MailItemsAccessed, and cannot extend retention in-product.

Retention is per-user by licence, not per-tenant: in a mixed tenant the same query can return a year of history for an E5 custodian and 180 days for the E3 account next to them. Confirm the licence on the account you are investigating before you read an empty result as "no activity". State that gap explicitly. Check SIEM or Log Analytics for extended retention.

Unified Audit Log (UAL)

The UAL is the single richest data source. Every investigation starts here.

Connect-ExchangeOnline -UserPrincipalName admin@tenant.onmicrosoft.com

# Basic search -- always constrain by date and user
Search-UnifiedAuditLog -StartDate "2026-07-01" -EndDate "2026-07-20" `
  -UserIds compromised@contoso.com -ResultSize 5000

# Search specific operations
Search-UnifiedAuditLog -StartDate "2026-07-01" -EndDate "2026-07-20" `
  -Operations "New-InboxRule","Set-InboxRule","Set-Mailbox","Add-MailboxPermission" `
  -ResultSize 5000

# MailItemsAccessed (E5 only)
Search-UnifiedAuditLog -StartDate "2026-07-01" -EndDate "2026-07-20" `
  -Operations MailItemsAccessed -UserIds compromised@contoso.com -ResultSize 5000

# Export -- AuditData field is JSON, expand it
Search-UnifiedAuditLog -StartDate "2026-07-01" -EndDate "2026-07-20" `
  -UserIds compromised@contoso.com -ResultSize 5000 |
  Select-Object CreationDate, UserIds, Operations,
    @{N='AuditData';E={$_.AuditData | ConvertFrom-Json | ConvertTo-Json -Depth 10}} |
  Export-Csv -Path .\ual_export.csv -NoTypeInformation

Key UAL operations

OperationSignificance
MailItemsAccessedBind = single item read, Sync = bulk download. Bulk sync is the BEC exfiltration indicator.
New-InboxRule / Set-InboxRuleRules hiding replies or forwarding. Check for keyword targets: "invoice", "payment", "security".
Set-MailboxForwardingSMTPAddress or ForwardingAddress changed -- silent external forwarding.
Add-MailboxPermissionFullAccess or SendAs delegation -- persistence.
Consent to applicationOAuth grant. AuditData contains the permissions.
Add service principal credentialsNew secret/certificate on an app registration.
HardDelete / SoftDeleteEvidence destruction -- attacker deleting sent items.

The UAL caps at 50,000 results per query. Narrow the date range if you hit it.

Entra ID Sign-In Analysis

Query sign-in logs and look for:

// Sign-ins from the compromised account
SigninLogs
| where UserPrincipalName == "compromised@contoso.com"
| where TimeGenerated > ago(30d)
| project TimeGenerated, AppDisplayName, IPAddress, Location,
    ClientAppUsed, ResultType, AuthenticationRequirement, MfaDetail, RiskState
| order by TimeGenerated asc

// Legacy auth sign-ins that bypass MFA
SigninLogs
| where ClientAppUsed in ("IMAP4", "POP3", "SMTP", "Exchange ActiveSync",
    "MAPI Over HTTP", "Outlook Anywhere", "Exchange Web Services")
| where ResultType == 0
| summarize count() by UserPrincipalName, ClientAppUsed, IPAddress

MFA analysis

Illicit consent grants are the most-missed persistence in M365 compromise.

Connect-MgGraph -Scopes "Application.Read.All","Directory.Read.All"

# Delegated permission grants -- look for Mail.Read, Files.ReadWrite, etc.
Get-MgOauth2PermissionGrant -All | Where-Object {
    $_.Scope -match "Mail.Read|Mail.ReadWrite|Files.ReadWrite|User.Read.All"
} | Format-Table ClientId, ConsentType, Scope, PrincipalId

# Application permission assignments
Get-MgServicePrincipal -All | ForEach-Object {
    $sp = $_
    Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -ErrorAction SilentlyContinue |
    Select-Object @{N='AppName';E={$sp.DisplayName}}, @{N='AppId';E={$sp.AppId}},
                  PrincipalDisplayName, @{N='Created';E={$_.CreatedDateTime}}
}
PermissionRisk
Mail.Read / Mail.ReadWriteEmail exfiltration. Suspicious on unknown apps.
Files.ReadWrite.AllFull SharePoint/OneDrive access.
Directory.ReadWrite.AllCan modify users, groups, roles.
full_access_as_appExchange app-level mailbox access -- almost never legitimate for third parties.

Distinguish delegated consent (one user's data) from admin consent (tenant-wide). Admin consent to a malicious app exposes every user.

Mailbox Rule Forensics

Inbox rules persist after password resets. Always check them.

# Inbox rules
Get-InboxRule -Mailbox compromised@contoso.com |
  Select-Object Name, Enabled, MoveToFolder, DeleteMessage, ForwardTo,
    RedirectTo, MarkAsRead | Format-List

# Mailbox-level forwarding
Get-Mailbox -Identity compromised@contoso.com |
  Select-Object ForwardingSMTPAddress, ForwardingAddress, DeliverToMailboxAndForward

# All mailboxes with external forwarding
Get-Mailbox -ResultSize Unlimited |
  Where-Object { $_.ForwardingSMTPAddress -ne $null } |
  Select-Object UserPrincipalName, ForwardingSMTPAddress

# Transport rules
Get-TransportRule | Where-Object { $_.RedirectMessageTo -or $_.BlindCopyTo } |
  Select-Object Name, State, RedirectMessageTo, BlindCopyTo

# Delegate access
Get-MailboxPermission -Identity compromised@contoso.com |
  Where-Object { $_.User -ne "NT AUTHORITY\SELF" -and -not $_.IsInherited }

Red flags: rules targeting keywords ("security", "password", "MFA"), rules deleting or moving to RSS Feeds / Conversation History, blank-named rules, forwarding to free email providers, rules created after the first suspicious sign-in.

Connect-IPPSSession -UserPrincipalName admin@tenant.onmicrosoft.com

# Preserve mailbox content -- do this early
Set-Mailbox -Identity compromised@contoso.com -LitigationHoldEnabled $true

# Search outbound mail from compromised account
New-ComplianceSearch -Name "IR-2026-042 Outbound" `
  -ExchangeLocation compromised@contoso.com `
  -ContentMatchQuery "sent>=2026-07-01 AND sent<=2026-07-20"
Start-ComplianceSearch -Identity "IR-2026-042 Outbound"

Place litigation holds before retention policies or the attacker can purge evidence.

Azure AD Audit Log Analysis

// Role assignments
AuditLogs
| where OperationName == "Add member to role"
| extend TargetUser = tostring(TargetResources[0].userPrincipalName),
         RoleName = tostring(TargetResources[0].modifiedProperties[1].newValue)
| project TimeGenerated, InitiatedBy, TargetUser, RoleName

// Service principal credential changes (persistence)
AuditLogs
| where OperationName in ("Add service principal credentials",
    "Update application - Certificates and secrets management")
| project TimeGenerated, InitiatedBy, TargetResources

// Conditional Access policy changes
AuditLogs
| where OperationName has "conditional access"
| project TimeGenerated, OperationName, InitiatedBy, Result

// MFA method changes (attacker registering their own factor)
AuditLogs
| where OperationName in ("User registered security info",
    "Admin registered security info", "User deleted security info")
| project TimeGenerated, OperationName, InitiatedBy, TargetResources

Attackers with Application Admin or Global Admin can add credentials to existing app registrations for persistent, MFA-independent access:

Get-MgApplication -All | ForEach-Object {
    $app = $_
    $app.PasswordCredentials + $app.KeyCredentials | Where-Object {
        $_.StartDateTime -gt (Get-Date).AddDays(-30)
    } | Select-Object @{N='App';E={$app.DisplayName}}, @{N='AppId';E={$app.AppId}},
                      StartDateTime, EndDateTime
}

Timeline Construction

Correlate across the three primary log sources:

Sign-in logs  -->  When and where the attacker authenticated
Audit logs    -->  Configuration changes they made
UAL           -->  Data they accessed or modified
  1. Anchor on the earliest suspicious sign-in
  2. Pull sign-in, UAL, and audit events for that user +/- 7 days
  3. Merge into a single UTC timeline
  4. Look for the pattern: sign-in, reconnaissance, persistence (rule/forwarding/OAuth), action on objectives
UTC Timestamp        | Source  | Event                                    | Detail
2026-07-12 08:41:22  | SignIn  | Sign-in from 198.51.x.x                  | Nigeria, no MFA
2026-07-12 08:42:05  | UAL     | MailItemsAccessed (Sync)                 | 847 items via Graph
2026-07-12 08:43:18  | UAL     | New-InboxRule "."                        | Delete "security alert"
2026-07-12 08:44:01  | UAL     | Set-Mailbox                              | ForwardingSMTPAddress set
2026-07-12 08:45:33  | UAL     | Consent to application                   | Mail.Read, Mail.Send
2026-07-12 09:12:44  | UAL     | Send (SendAs)                            | Invoice redirect to vendor

Containment

Execute simultaneously once scoping is complete. Partial containment alerts the attacker.

# Revoke sessions and reset credentials
Revoke-MgUserSignInSession -UserId compromised@contoso.com
Update-MgUser -UserId compromised@contoso.com -PasswordProfile @{
    Password = (New-Guid).Guid + "!Aa1"; ForceChangePasswordNextSignIn = $true }

# Disable account if active compromise is ongoing
Update-MgUser -UserId compromised@contoso.com -AccountEnabled:$false

# Remove attacker inbox rules
Get-InboxRule -Mailbox compromised@contoso.com |
  Where-Object { $_.Name -match "^\.$|^$" -or $_.DeleteMessage -eq $true } |
  Remove-InboxRule -Confirm:$false

# Remove forwarding
Set-Mailbox -Identity compromised@contoso.com `
  -ForwardingSMTPAddress $null -ForwardingAddress $null `
  -DeliverToMailboxAndForward $false

# Remove unauthorized delegate access
Get-MailboxPermission -Identity compromised@contoso.com |
  Where-Object { $_.User -ne "NT AUTHORITY\SELF" -and -not $_.IsInherited } |
  ForEach-Object { Remove-MailboxPermission -Identity compromised@contoso.com `
    -User $_.User -AccessRights $_.AccessRights -Confirm:$false }

# Block malicious OAuth app
$sp = Get-MgServicePrincipal -Filter "appId eq '<malicious-app-id>'"
Update-MgServicePrincipal -ServicePrincipalId $sp.Id -AccountEnabled:$false
Get-MgOauth2PermissionGrant -Filter "clientId eq '$($sp.Id)'" |
  Remove-MgOauth2PermissionGrant

Session revocation alone is insufficient -- tokens may remain valid up to one hour. Disable the account for immediate lockout. After containment, verify: no forwarding remains, no unknown delegates, no unknown OAuth grants, no attacker-registered MFA methods, no service principal credentials from the compromise window.

Rationalizations to Reject

grants, inbox rules, forwarding, and delegate access all survive a password reset. Revoke sessions and audit every persistence mechanism.

is unavailable, but sign-in logs, UAL operations, inbox rules, and forwarding still exist. State the gap and work with what you have.

exit IP, user agent, and timing against established patterns. Attackers use VPNs too.

the right license tier and policy config. Absence of alerts is not evidence of absence.

a valid refresh token give persistent access without needing the password again.

Revoke the tokens and credentials, not just the network path.

That is your visibility limit, not the attacker's timeline. Document the limitation and check SIEM for extended retention.

References