Author:
David Nehoda, Technical Solutions Consultant
Entra ID, Office 365, Defender, and End-to-End Attack Chains
---
Overview
This section covers cloud Microsoft infrastructure: Entra ID (Azure AD), Office 365 (Exchange, SharePoint, Teams), and Defender for Endpoint. Combined with on-premises data, these sources enable end-to-end attack chain detection from phishing through Kerberos abuse to data exfiltration.
Section 1: Entra ID (Azure AD)
Entra ID is not the same source as on-prem AD. Different schema, different field names, different UDM mappings. Most teams trip here because they assume on-prem rules work on cloud logs. They do not.
Key Log Sources
| Source | Description | Priority |
| Entra ID Sign-in logs | Interactive, non-interactive, service principal sign-ins | Critical |
| Entra ID Audit logs | Directory changes, role assignments, application consent | Critical |
| Risky sign-ins | Microsoft Identity Protection verdicts (anonymous IP, unfamiliar location, leaked creds) | High |
| Risky users | Users flagged as at-risk | High |
| Provisioning logs | SCIM provisioning from/to Entra ID | Medium |
Critical Operations and Events
| Operation | UDM Event Type | Threat |
| Add application | USER_RESOURCE_CREATION | Rogue app registration for consent phishing |
| Consent to application | USER_RESOURCE_UPDATE_PERMISSIONS | Illicit consent grant, cloud equivalent of admin shell |
| Add OAuth2PermissionGrant | USER_RESOURCE_UPDATE_PERMISSIONS | Granting Graph API scopes to attacker-controlled app |
| Add member to role | GROUP_MODIFICATION | Granting Global Admin, Privileged Role Admin |
| Update user (sensitive fields) | USER_RESOURCE_UPDATE_CONTENT | Attacker modifying MFA, authentication methods |
| Add authentication method | USER_CHANGE_PASSWORD | Attacker enrolling new MFA device |
| Disable account or Delete user | USER_DELETION | Anti-forensics, denial of service against defenders |
| Risky sign-in | USER_LOGIN | Anonymous IP, atypical travel—account takeover indicators |
| Sign-in with CA Failure | USER_LOGIN | MFA prompts, location blocks, compliant device blocks |
Field Mapping: Entra ID
| Microsoft Field | UDM Field | Notes |
| userPrincipalName | principal.user.email_addresses or principal.user.userid | Cloud identity |
| ipAddress (sign-in) | principal.ip | Source IP |
| location.city, location.countryOrRegion | principal.location.city, principal.location.country_or_region | For impossible travel detection |
| appDisplayName | target.application | What app the user signed in to |
| resourceDisplayName | target.resource.name | Resource accessed |
| clientAppUsed | network.http.user_agent | Browser, rich client, mobile app, legacy auth |
| riskLevel | security_result.risk_score or security_result.detection_fields[RiskLevel] | Identity Protection verdict |
| conditionalAccessStatus | security_result.action (ALLOW, BLOCK, CHALLENGE) | CA policy result |
| authenticationDetails | extensions.auth.auth_details | MFA, password, certificate, FIDO2 |
| initiatedBy.user.userPrincipalName | principal.user.email_addresses | For audit logs, who did it |
| targetResources[0].userPrincipalName | target.user.email_addresses | For audit logs, who got changed |
YARA-L Rules: Entra ID
#### Rule 1: Illicit OAuth Consent Grant (Consent Phishing)
The attack Microsoft's own docs underplay. User clicks link, grants `offline_access` and `Mail.Read` to attacker's app, attacker reads mail forever without knowing password.
rule Detect_Illicit_OAuth_Consent_Grant {
meta:
author = "Detection Engineering"
description = "Consent to application or OAuth2 permission grant that includes high-risk scopes (Mail.Read, Files.Read.All, offline_access). Filter against known-good app allowlist."
severity = "HIGH"
mitre_attack = "T1528"
events:
$consent.metadata.log_type = "AZURE_AD"
($consent.metadata.product_event_type = "Consent to application" or
$consent.metadata.product_event_type = "Add OAuth2PermissionGrant" or
$consent.metadata.product_event_type = "Add delegated permission grant")
re.regex($consent.security_result.detection_fields["Scope"], `(?i).*(Mail\.Read|Files\.Read\.All|Sites\.Read\.All|offline_access|Mail\.Send|full_access_as_user).*`)
not $consent.target.application in %approved_oauth_apps
$consent.principal.user.email_addresses = $user
match:
$user over 1h
outcome:
$scopes = array_distinct($consent.security_result.detection_fields["Scope"])
$apps = array_distinct($consent.target.application)
condition:
$consent
}
#### Rule 2: Impossible Travel
rule Detect_Impossible_Travel_EntraID {
meta:
author = "Detection Engineering"
description = "Two successful sign-ins for same user from distinct countries within 2 hours. Filters out known VPN exit IPs and service principals."
severity = "HIGH"
mitre_attack = "T1078.004"
events:
$s1.metadata.log_type = "AZURE_AD"
$s1.metadata.event_type = "USER_LOGIN"
$s1.security_result.action = "ALLOW"
$s1.principal.user.email_addresses = $user
$s1.principal.location.country_or_region = $c1
$s2.metadata.log_type = "AZURE_AD"
$s2.metadata.event_type = "USER_LOGIN"
$s2.security_result.action = "ALLOW"
$s2.principal.user.email_addresses = $user
$s2.principal.location.country_or_region = $c2
$c1 != $c2
$s2.metadata.event_timestamp.seconds > $s1.metadata.event_timestamp.seconds
$s2.metadata.event_timestamp.seconds < $s1.metadata.event_timestamp.seconds + 7200
match:
$user over 2h
outcome:
$countries = array_distinct($c1)
$countries_second = array_distinct($c2)
condition:
$s1 and $s2
}
#### Rule 3: MFA Fatigue (Push Bombing)
rule Detect_MFA_Push_Bombing {
meta:
author = "Detection Engineering"
description = "Burst of MFA challenges for one user, followed by successful auth. Pattern of Nobelium, Lapsus$, and most modern AiTM phishing kits."
severity = "HIGH"
mitre_attack = "T1621"
events:
$fail.metadata.log_type = "AZURE_AD"
$fail.metadata.event_type = "USER_LOGIN"
$fail.security_result.action = "BLOCK"
re.regex($fail.security_result.detection_fields["StatusReason"], `(?i).*(MFA|authentication failed|user declined).*`)
$fail.principal.user.email_addresses = $user
match:
$user over 10m
outcome:
$fail_count = count($fail)
condition:
$fail and $fail_count >= 5
}
Pair this with the second rule correlating MFA burst to subsequent successful sign-in for true push-bombing confirmation.
Section 2: Office 365 and Exchange Online
The Management Activity API is where Business Email Compromise, insider exfiltration, and consent phishing live. The operations to collect are not the ones Microsoft's default compliance policies enable.
Critical O365 Operations
| Operation | UDM Event Type | Threat | Priority |
| MailboxLogin (non-owner) | USER_LOGIN | Delegate or admin abuse, MFA bypass | HIGH |
| New-InboxRule | EMAIL_TRANSACTION or USER_RESOURCE_UPDATE_CONTENT | BEC concealment, auto-forward, auto-delete alerts | CRITICAL |
| Set-InboxRule | USER_RESOURCE_UPDATE_CONTENT | Modifying existing rules | HIGH |
| Add-MailboxPermission | USER_RESOURCE_UPDATE_PERMISSIONS | Persistent delegate access for monitoring | CRITICAL |
| Set-Mailbox with ForwardingSmtpAddress | USER_RESOURCE_UPDATE_CONTENT | Transport-level forwarding bypasses inbox rules | CRITICAL |
| FileDownloaded | NETWORK_HTTP | Mass exfiltration from SharePoint or OneDrive | HIGH |
| FileShared | USER_RESOURCE_UPDATE_PERMISSIONS | Sharing sensitive files externally | HIGH |
| AnonymousLinkCreated | USER_RESOURCE_UPDATE_PERMISSIONS | Public link creation for internal docs | CRITICAL |
| UserLoggedIn (unusual location) | USER_LOGIN | Account takeover indicator | HIGH |
| Add-RoleGroupMember | GROUP_MODIFICATION | Granting Exchange admin | CRITICAL |
| Set-AdminAuditLogConfig | SETTING_MODIFICATION | Disabling admin audit logging, anti-forensics | CRITICAL |
| New-ComplianceSearch | USER_RESOURCE_CREATION | eDiscovery abuse, content collection by attacker | CRITICAL |
| New-ComplianceSearchAction (Export) | USER_RESOURCE_UPDATE_CONTENT | eDiscovery export—data leaves tenant | CRITICAL |
| Add-MailboxFolderPermission (Owner) | USER_RESOURCE_UPDATE_PERMISSIONS | Mailbox folder takeover | HIGH |
Field Mapping: O365
| Microsoft Field | UDM Field | Notes |
| Operation | metadata.product_event_type | Operation name verbatim |
| UserId | principal.user.email_addresses | Account performing the action |
| ClientIP | principal.ip | Source IP (sometimes Exchange service IP for admin commands) |
| Parameters.ForwardTo or Parameters.RedirectTo | security_result.detection_fields[ForwardTo] | External forwarding target |
| Parameters.Name | target.resource.name | Rule or resource name |
| SiteUrl | target.url | SharePoint or OneDrive site |
| SourceFileName | target.file.full_path | Downloaded or shared file |
| ObjectId | target.resource.id | Mailbox or resource identifier |
| RecordType | metadata.product_log_id | 1=Azure AD, 2=Exchange Admin, 6=SharePoint File, etc. |
YARA-L Rules: Office 365
#### Rule 4: External Forwarding Inbox Rule (BEC Fingerprint)
rule Detect_O365_External_Forwarding_Inbox_Rule {
meta:
author = "Detection Engineering"
description = "New or modified inbox rule that forwards/redirects to external address, or moves finance/security keyword messages to Deleted Items. Classic BEC concealment."
severity = "CRITICAL"
mitre_attack = "T1114.003"
events:
$o365.metadata.log_type = "O365"
($o365.metadata.product_event_type = "New-InboxRule" or
$o365.metadata.product_event_type = "Set-InboxRule" or
$o365.metadata.product_event_type = "Set-Mailbox")
(re.regex($o365.security_result.detection_fields["ForwardTo"], `(?i)@(gmail|proton|yahoo|hotmail|outlook|icloud|yandex|mail\.ru|aol|tutanota)\.`) or
re.regex($o365.security_result.detection_fields["RedirectTo"], `(?i)@(gmail|proton|yahoo|hotmail|outlook|icloud|yandex|mail\.ru|aol|tutanota)\.`) or
re.regex($o365.security_result.detection_fields["ForwardingSmtpAddress"], `(?i)smtp:.*@.*`) or
(re.regex($o365.target.resource.name, `(?i).*(invoice|payment|wire|transfer|security|login|alert|suspic).*`) and
re.regex($o365.security_result.detection_fields["MoveToFolder"], `(?i).*(Junk|RSS|Deleted|Archive|Conversation History).*`)))
$o365.principal.user.email_addresses = $victim
match:
$victim over 1h
outcome:
$rule_names = array_distinct($o365.target.resource.name)
$forward_targets = array_distinct($o365.security_result.detection_fields["ForwardTo"])
condition:
$o365
}
#### Rule 5: Mass SharePoint/OneDrive Download
rule Detect_Mass_SharePoint_Download {
meta:
author = "Detection Engineering"
description = "One user downloading 100+ files from SharePoint/OneDrive in 10-minute window. Catches insider exfiltration and account takeover data pulls."
severity = "HIGH"
mitre_attack = "T1530"
events:
$dl.metadata.log_type = "O365"
$dl.metadata.product_event_type = "FileDownloaded"
$dl.principal.user.email_addresses = $user
match:
$user over 10m
outcome:
$file_count = count($dl)
$unique_files = count_distinct($dl.target.file.full_path)
$sites = array_distinct($dl.target.url)
condition:
$dl and $file_count > 100
}
#### Rule 6: eDiscovery Export Abuse
Attackers who obtain Compliance Administrator credentials use eDiscovery to quietly search and export mailbox content. One of the highest-impact post-compromise TTPs with almost no detection.
rule Detect_eDiscovery_Export_Activity {
meta:
author = "Detection Engineering"
description = "Compliance search export/preview by account not on approved eDiscovery operator list."
severity = "CRITICAL"
mitre_attack = "T1213"
events:
$ed.metadata.log_type = "O365"
($ed.metadata.product_event_type = "New-ComplianceSearchAction" or
$ed.metadata.product_event_type = "Start-ComplianceSearch" or
$ed.metadata.product_event_type = "New-ComplianceSearch")
not $ed.principal.user.email_addresses in %approved_ediscovery_operators
$ed.principal.user.email_addresses = $actor
match:
$actor over 1h
outcome:
$searches = array_distinct($ed.target.resource.name)
$operations = array_distinct($ed.metadata.product_event_type)
condition:
$ed
}
Section 3: Microsoft Defender for Endpoint
Defender generates two telemetry streams: alerts (high signal, pre-triaged) and raw device events (Sysmon-equivalent, high volume). Ingest both only if sure you need them—raw stream duplicates Sysmon at 80% overlap.
Alert Stream Field Mapping
| Microsoft Field | UDM Field | Notes |
| Title | security_result.summary | Alert title |
| Severity | security_result.severity | UDM enum: LOW, MEDIUM, HIGH, CRITICAL |
| Category | security_result.category_details | Defender threat category |
| AlertId | metadata.product_log_id | Unique ID for correlation |
| DetectionSource | security_result.detection_fields[DetectionSource] | EDR, AV, SmartScreen, Automated Investigation |
| MachineId | target.asset_id | Defender-specific device identifier |
| MachineName or ComputerDnsName | target.hostname | Affected endpoint |
| Sha256 (evidence) | target.file.sha256 | File hash from alert |
| AccountName or AccountUpn | target.user.userid or target.user.email_addresses | Affected user |
| RelatedUser | target.user.userid | Distinct from AccountName in some alerts |
| RemediationAction | security_result.action_details | What Defender did (quarantine, block, allow) |
YARA-L Rules: Defender
#### Rule 7: Defender Alert Followed by Outbound Connection
The C-suite question: did malware beacon before Defender killed it?
rule Correlate_Defender_Alert_With_Post_Alert_Beacon {
meta:
author = "Detection Engineering"
description = "Defender alert on a host followed by successful outbound connection within 5 minutes. Beacon may be attacker traffic that succeeded before containment."
severity = "CRITICAL"
mitre_attack = "T1071"
events:
$def.metadata.log_type = "WINDOWS_DEFENDER_ATP"
$def.metadata.event_type = "SECURITY_ALERT"
$def.target.hostname = $host
$net.metadata.event_type = "NETWORK_CONNECTION"
$net.security_result.action = "ALLOW"
$net.principal.hostname = $host
$net.metadata.event_timestamp.seconds >= $def.metadata.event_timestamp.seconds
$net.metadata.event_timestamp.seconds <= $def.metadata.event_timestamp.seconds + 300
not $net.target.ip in %known_corporate_ranges
match:
$host over 10m
outcome:
$c2_destinations = array_distinct($net.target.ip)
$alert_summary = array_distinct($def.security_result.summary)
condition:
$def and $net
}
#### Rule 8: Defender Tampering Attempt
rule Detect_Defender_Tampering_Attempt {
meta:
author = "Detection Engineering"
description = "Commands, registry changes, services targeting Defender components. Covers Set-MpPreference exclusions, service stops, policy registry writes."
severity = "HIGH"
mitre_attack = "T1562.001"
events:
$tamper.principal.hostname = $host
(($tamper.metadata.event_type = "PROCESS_LAUNCH" and
(re.regex($tamper.target.process.command_line, `(?i).*Set-MpPreference.*(-Disable|ExclusionPath|ExclusionProcess|MAPSReporting\s+0|SubmitSamplesConsent\s+0).*`) or
re.regex($tamper.target.process.command_line, `(?i).*(sc\s+(stop|delete|config)\s+(WinDefend|Sense|WdNisSvc)).*`) or
re.regex($tamper.target.process.command_line, `(?i).*(Add-MpPreference\s+-ExclusionPath).*`) or
re.regex($tamper.target.process.command_line, `(?i).*(DisableAntiSpyware|DisableRealtimeMonitoring|DisableBehaviorMonitoring).*`))) or
($tamper.metadata.event_type = "REGISTRY_MODIFICATION" and
re.regex($tamper.target.registry.registry_key, `(?i).*\\Microsoft\\Windows Defender\\(Real-Time Protection|Exclusions|Policy Manager).*`)))
match:
$host over 10m
outcome:
$actions = array_distinct($tamper.target.process.command_line)
$reg_keys = array_distinct($tamper.target.registry.registry_key)
condition:
$tamper
}
Section 4: Reference Lists and Data Tables
Every rule in this guide assumes you maintain these. A rule without context joins is a rule that storms the SOC. A rule with reference-list exclusions fires only on the real thing.
Reference Lists to Build
| Name | Type | Purpose | Seed With |
| `approved_oauth_apps` | STRING | Entra ID app display names that have been vetted | Output of `Get-MgServicePrincipal` filtered by publisher |
| `approved_ediscovery_operators` | STRING | UPNs that legitimately run eDiscovery | Your eDiscovery team roster |
| `known_corporate_ranges` | CIDR | IP ranges of offices and VPN exits | Network team CMDB |
| `high_risk_geo_countries` | STRING | ISO country codes to flag in sign-in rules | Your threat model—sanctioned countries + no-business locations |
Data Tables for Multi-Column Lookups
When a reference list is not enough, use a data table. Example: service accounts with expected source hosts.
| sam_account_name | allowed_source_host |
|---------------------------------------|---------------------------------------|
| svc_sql_prod | sqlprod-01.corp.local |
| svc_sql_prod | sqlprod-02.corp.local |
| svc_backup_daily | backup-01.corp.local |
A YARA-L rule can then detect "service account logged in from host not in its approved list" without hardcoding entire mapping inside the rule.
Section 5: The Cross-Source Attack Chain Rule
The rule the executive summary of every detection engineering proposal promises and almost none deliver. Correlates O365 phishing sign-in, Sysmon PowerShell on same user's workstation, Kerberos ticket anomaly—all keyed off user identity.
rule Detect_Cross_Source_Phish_to_Kerberos_Abuse {
meta:
author = "Detection Engineering"
description = "End-to-end attack chain: risky O365 sign-in → suspicious PowerShell on user's workstation → Kerberos request with RC4. Any one stage is noisy. All three in sequence within 4 hours is a real incident."
severity = "CRITICAL"
mitre_attack = "T1566.002, T1059.001, T1558.003"
events:
$phish.metadata.log_type = "O365"
$phish.metadata.event_type = "USER_LOGIN"
$phish.security_result.action = "ALLOW"
(re.regex($phish.security_result.detection_fields["RiskLevel"], `(?i)(high|medium)`) or
re.regex($phish.principal.ip, `.*`))
$phish.principal.user.email_addresses = $user_email
$ps.metadata.event_type = "PROCESS_LAUNCH"
re.regex($ps.target.process.file.full_path, `(?i).*\\powershell(_ise)?\.exe$`)
re.regex($ps.target.process.command_line, `(?i).*(-enc|FromBase64String|DownloadString|IEX|Invoke-Expression|bypass|-nop|-w\s+hidden).*`)
$ps.target.user.userid = $user_id
$krb.metadata.log_type = "WINEVTLOG"
($krb.metadata.product_event_type = "4768" or $krb.metadata.product_event_type = "4769")
$krb.security_result.detection_fields["TicketEncryptionType"] = "0x17"
$krb.principal.user.userid = $user_id
$ps.metadata.event_timestamp.seconds >= $phish.metadata.event_timestamp.seconds
$ps.metadata.event_timestamp.seconds <= $phish.metadata.event_timestamp.seconds + 14400
$krb.metadata.event_timestamp.seconds >= $ps.metadata.event_timestamp.seconds
$krb.metadata.event_timestamp.seconds <= $ps.metadata.event_timestamp.seconds + 14400
match:
$user_id over 4h
outcome:
$sign_in_ip = array_distinct($phish.principal.ip)
$powershell_cmd = array_distinct($ps.target.process.command_line)
$kerberos_host = array_distinct($krb.principal.hostname)
$kerberos_targets = array_distinct($krb.target.application)
condition:
$phish and $ps and $krb
}
Note: Mapping `user_email` to `user_id` requires that on-prem UPN matches O365 UPN. If not, use a data table to resolve.
Section 6: Validating Rules Before Shipping
The YARA-L Validator MCP generates synthetic UDM events from a rule's trigger conditions, ingests them into Chronicle, and confirms the rule fires.
**Workflow:**
1. Write the rule
2. Paste into validator
3. Validator extracts UDM fields the rule reads, generates events satisfying them, ingests
4. Validator polls Chronicle until rule fires or times out
5. Optional: generate perturbations to prove rule doesn't fire on benign traffic
**Cache the passing events as a fixture.** Every future rule edit is regression-tested against the same fixture.
**What the Validator Cannot Do:**
Composite rules (rules chaining detections from other rules) cannot be fast-validated. Chronicle evaluates composites on HOURLY or DAILY schedules. Use validator overnight.
Section 7: Conclusion
Microsoft's telemetry is not broken. It is fragmented by design. What breaks is the detection logic depending on those schemas.
Google SecOps with strict UDM mapping solves this. One field path (`principal.user.userid`) matches on-prem AD, Entra ID, O365, and Defender. One event type (`PROCESS_LAUNCH`) matches Sysmon and Security 4688. One rule tracks an adversary from phishing link to forged Kerberos ticket without a single vendor-specific reference.
**The rules in this guide fire today. They will still fire after Microsoft's next schema revision, because UDM absorbs the change at the parser layer.**
Key Takeaways
✅ Entra ID is not on-prem AD—different field names, different event types
✅ O365 is the primary vector for BEC, eDiscovery abuse, and persistence
✅ Defender catches some, misses others—YARA-L rules are your second layer
✅ Cross-source rules detect end-to-end attacks, not individual events
✅ Reference lists reduce false positives 100x over
✅ Validate rules before shipping, not after they go noisy
**Deploy with confidence. These rules survive Microsoft's next update.**
