Investigating M365 Entra logo

Investigating M365 Entra

Community
trilwu
investigating-m365-entra

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.

Overview

Publishertrilwu
Repositorysecskills
Skill nameinvestigating-m365-entra
Stars
144
Forks
15
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by trilwu on GitHub. Read the source before you install it.

Installation

Install the Investigating M365 Entra AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/trilwu/secskills.git /tmp/secskills
mkdir -p .claude/skills
cp -r /tmp/secskills/secskills-defense/skills/investigating-m365-entra .claude/skills/investigating-m365-entra
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Investigating M365 Entra in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Investigating M365 Entra on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Investigating M365 Entra is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Investigating M365 and Entra ID

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

  • Business email compromise (BEC) -- unauthorized mailbox access, forwarding, or impersonation
  • Entra ID account compromise -- suspicious sign-ins, token replay, credential stuffing
  • Suspicious OAuth consent grants -- third-party apps with excessive permissions
  • Mailbox rule manipulation -- inbox rules hiding attacker communications or forwarding mail
  • Azure AD sign-in anomalies -- impossible travel, legacy auth, anonymizer networks
  • Conditional Access or MFA tampering -- policy changes, MFA fatigue attacks

When NOT to Use

  • Broader incident response methodology -- use responding-to-incidents
  • Offensive testing of Entra ID -- use attacking-entra-id
  • An Azure resource/subscription compromise -- use investigating-azure-incidents
  • An AWS compromise -- use investigating-aws-incidents
  • Other cloud infrastructure (GCP, offensive testing) -- use exploiting-cloud-platforms
  • Building detection rules from findings -- use engineering-detections

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.

powershell
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:

  • Impossible travel -- distant locations within an impossible timeframe
  • Anonymous IPs -- Tor exits, VPN services, known anonymizers
  • Legacy auth -- IMAP, POP3, SMTP AUTH bypass MFA unless CA blocks them
  • Token replay -- same correlation ID from different source IPs
  • Anomalous user agents -- Python requests, PowerShell hitting OWA or Graph
kusto
// 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

  • ResultType 50074 -- MFA required, not completed
  • ResultType 50076 -- MFA completed (attacker had the factor or used MFA fatigue)
  • AuthenticationRequirement = singleFactorAuthentication -- MFA not required (CA gap)
  • Check MfaDetail -- push acceptance after repeated prompts = MFA fatigue

OAuth and Consent Grant Investigation

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

powershell
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.

powershell
# 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.

eDiscovery and Content Search

powershell
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

kusto
// 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:

powershell
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.

powershell
# 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

  • "We reset the password, so the account is secure." Refresh tokens, OAuth grants, inbox rules, forwarding, and delegate access all survive a password reset. Revoke sessions and audit every persistence mechanism.
  • "We only have E3, so we cannot investigate mail access." MailItemsAccessed is unavailable, but sign-in logs, UAL operations, inbox rules, and forwarding still exist. State the gap and work with what you have.
  • "The sign-in was from a VPN, so it is probably the user." Correlate the exit IP, user agent, and timing against established patterns. Attackers use VPNs too.
  • "No alerts fired in Defender, so there is no compromise." Defender requires the right license tier and policy config. Absence of alerts is not evidence of absence.
  • "The OAuth app only has delegated permissions." Delegated permissions with a valid refresh token give persistent access without needing the password again.
  • "We blocked the IP, so the attacker is locked out." Attackers rotate IPs. Revoke the tokens and credentials, not just the network path.
  • "Logs only go back 30 days, so the compromise started within that window." That is your visibility limit, not the attacker's timeline. Document the limitation and check SIEM for extended retention.

References

  • responding-to-incidents -- broader IR methodology and evidence handling
  • attacking-entra-id -- offensive Entra ID techniques, useful for understanding attacker methods
  • exploiting-cloud-platforms -- cloud infrastructure attacks beyond M365
  • engineering-detections -- building detection rules from investigation findings
  • hunting-threats -- proactive hunting in M365 and Entra ID telemetry

Frequently asked questions

What does the Investigating M365 Entra AI skill do?

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.

Why use Investigating M365 Entra on TypingMind?

Because you install it once and use it with any model. Investigating M365 Entra is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Investigating M365 Entra in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/trilwu/secskills/tree/main/secskills-defense/skills/investigating-m365-entra. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Investigating M365 Entra?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Investigating M365 Entra?

As many as you like. As long as a model supports skills, you can use Investigating M365 Entra with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Investigating M365 Entra AI skill free?

Yes. It is published on GitHub by trilwu under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇