Skip to content

Add analytic rule: End-user consent to app with mailbox + offline_access scopes - #15047

Open
eduardarbona (earbona23) wants to merge 4 commits into
Azure:masterfrom
earbona23:entra-consent-mailbox-offline
Open

Add analytic rule: End-user consent to app with mailbox + offline_access scopes#15047
eduardarbona (earbona23) wants to merge 4 commits into
Azure:masterfrom
earbona23:entra-consent-mailbox-offline

Conversation

@earbona23

Copy link
Copy Markdown

Scheduled analytic rule over AuditLogs (T1528). Complements 'Suspicious application consent for offline access': drops the known-app allowlist, pins the scope combination, and adds mailbox write/send scopes. KQL syntax validated with the Kusto.Language parser (0 diagnostics).

@v-atulyadav
v-atulyadav requested a lite review from Copilot September 3, 2026 13:07
@v-atulyadav v-atulyadav added Solution Solution specialty review needed Analytic Rules labels Sep 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to run its full agentic suite in this review.

Pull request overview

Adds a new scheduled Microsoft Sentinel analytic rule to detect risky end-user OAuth consent events granting offline_access plus high‑risk delegated mailbox scopes (T1528), aiming to improve fidelity vs. existing offline-access consent detections.

Changes:

  • Introduces a new YAML analytic rule backed by an AuditLogs KQL query.
  • Filters for end-user (non-AllPrincipals) consent and surfaces app/user/IP/user-agent context.
  • Adds entity mappings for Account, CloudApplication, and IP enrichment.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +51 to +53
| where ConsentFull has "offline_access"
| where ConsentFull has_any ("Mail.Read", "Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite")
| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope "]" *
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(tostring(InitiatedBy.user.ipAddress)), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))
| extend Name = tostring(split(GrantInitiatedByUserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName, '@', 1)[0])
Comment on lines +3 to +16
description: |
'Identifies an illicit consent grant where a non-admin user consents to an
application requesting high-risk delegated mailbox permissions
(Mail.Read, Mail.ReadWrite, Mail.Send or MailboxSettings.ReadWrite) together
with offline_access. offline_access returns a long-lived refresh token, so a
single successful end-user consent gives an attacker-controlled application
durable, silent read/send access to the victim mailbox without re-prompting
for MFA. This is a common precursor to business email compromise (BEC),
mailbox exfiltration and internal phishing. This rule complements
"Suspicious application consent for offline access": it drops the
known-application allowlist join and instead pins the high-fidelity scope
combination, and it additionally covers mailbox write/send scopes
(Mail.ReadWrite, Mail.Send, MailboxSettings.ReadWrite), not only read.
For AuditLogs schema see https://learn.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
Comment on lines +42 to +44
| extend AppDisplayName = tostring(TargetResource.displayName),
AppClientId = tolower(tostring(TargetResource.id)),
ModifiedProperties = TargetResource.modifiedProperties
Comment on lines +60 to +61
| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))
| extend Name = tostring(split(GrantInitiatedByUserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName, '@', 1)[0])
Comment on lines +72 to +73
- entityType: Account
fieldMappings:
@earbona23

eduardarbona (earbona23) commented Sep 3, 2026

Copy link
Copy Markdown
Author

This rule detects end-user consent to an app requesting mailbox scopes together with offline_access, the pattern behind illicit consent grants that hand an attacker a long lived refresh token without a password or an MFA prompt (MITRE T1528). Ready for review, happy to adjust the query or metadata to fit the repo conventions.

@earbona23

Copy link
Copy Markdown
Author

Thanks for the review — addressed the feedback:

  • has_any term-matching → exact scope membership. Kept has/has_any as a cheap term-indexed pre-filter, then split the parsed GrantScope on spaces and require exact scopes via set_has_element. This removes the over-match where Mail.Read tokenizes to mail/read and would hit e.g. Mail.ReadBasic.
  • Consolidated the two Account entity mappings into a single Account entity (FullName / Name / UPNSuffix / AadUserId) so one account entity is produced per alert.
  • Renamed AppClientId → ServicePrincipalId. On the consent event TargetResource.id is the service principal object id, not the app (client) id, so the old name was misleading.
  • Description literal block: dropped the wrapping single quotes (they'd render literally), and switched to the standard split(upn, '@')[0] / [1].

Left the User-Agent AdditionalDetails[0] read as-is for now: it's contextual enrichment only (a missing UA doesn't change the detection), and the mv-expand/summarize refactor reshapes the query more than the value warrants. Happy to add it if you'd prefer. KQL re-validated by the pipeline.

@v-rusraut

v-rusraut commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Hi eduardarbona (@earbona23) ,

Please resolve below validation error,
image
Once validation pass then please package the solution using the V3 tool - https://github.com/Azure/Azure-Sentinel/blob/master/Tools/Create-Azure-Sentinel-Solution/V3/README.md and resolve all copilot comment, Additionally, provide a screenshot of working analytic query.

…3-field schema max

DetectionTemplateSchemaValidation caps each entityMapping at 3 fieldMappings; the consolidated Account entity had 4. FullName is redundant (Name+UPNSuffix reconstruct the UPN, also projected as GrantInitiatedByUserPrincipalName). Keeps Name/UPNSuffix/AadUserId.
@earbona23

Copy link
Copy Markdown
Author

Hi v-rusraut, thanks for the review.

Validation error fixed — the consolidated Account entityMapping had 4 fieldMappings, but the schema caps each entityMapping at 3. Dropped the FullName identifier (redundant: Name + UPNSuffix reconstruct the UPN, and the full UPN is also projected as GrantInitiatedByUserPrincipalName, so it stays visible in the alert). The Account entity now maps Name / UPNSuffix / AadUserId. Pushed in the latest commit.

Copilot comments — all addressed: has_any term-matching → exact scope membership via set_has_element; the two Account mappings consolidated into a single Account entity (this validation fix trims that to the 3-field max); AppClientIdServicePrincipalId; description block-scalar wrapping quotes dropped; standard split(upn, '@')[0]/[1]. The remaining AdditionalDetails[0] User-Agent note is contextual enrichment only — a missing UA does not affect the detection — happy to switch to an mv-apply lookup if you prefer.

I will follow up with the V3-packaged solution and a screenshot of the query running.

@earbona23

Copy link
Copy Markdown
Author

v-rusraut — a quick question on the packaging step before I push a large diff.

This PR adds a single analytic rule to the existing Microsoft Entra ID solution. When I ran the V3 tool locally, it regenerated the entire solution mainTemplate.json — a ~2,200-line diff spanning the other 73 rules / workbooks / playbooks, not just this rule — which risks clobbering unrelated solution content and diverging from the canonical package.

For a single-rule addition like this, would you prefer that I (a) commit the full V3-regenerated package, or (b) is the analytic-rule YAML sufficient here, with the solution package regenerated on the pipeline/maintainer side? I want to avoid introducing an unintended large package diff. Happy to proceed either way — thanks!

@earbona23

Copy link
Copy Markdown
Author

v-rusraut — an update on the V3 packaging, with the specifics I ran into.

I added the rule to Data/Solution_AAD.json and ran createSolutionV3.ps1 against the Microsoft Entra ID solution. The regenerated package has two problems that make me hesitant to push it as-is:

  • It rewrites the whole solution, not just this rule — a ~3,400-line diff across mainTemplate.json, createUiDefinition.json and testParameters.json, reshaping the other 73 rules and 3 workbooks. This looks like packaging-tool version drift rather than real content change.
  • It regresses ARM-TTK. The committed package fails only IDs Should Be Derived From ResourceIDs (a pre-existing, tolerated failure). The regenerated mainTemplate.json adds a second failure — Template Should Not Contain Blanks — because the current tool emits empty groupByEntities / groupByAlertDetails / groupByCustomDetails arrays under incidentConfiguration.groupingConfiguration that the canonical template omits.

I'd rather not commit a package that takes the solution from 1 to 2 ARM-TTK failures and diverges this much from canonical. Could you point me to the tool version/branch the current Entra ID package was built with, or regenerate it on your side at merge? The rule content is ready either way — the entityMapping 3-field validation error is fixed, all Copilot comments are resolved, and the query follows the AuditLogs schema.

On the screenshot: the rule fires on AuditLogs Consent to application events, so a populated result needs a workspace with a matching consent grant. I can supply that separately — or, if it helps, confirm the query passes schema validation. Let me know which you'd prefer.

…ytic rule

Register the new rule in Solution_AAD.json and add it to the Microsoft Entra ID
V3 package: analyticRuleObject74 variable, its contentTemplate resource, and the
contentPackages dependency in mainTemplate.json; the analytics element in
createUiDefinition.json; the 3.3.17 package zip; and the ReleaseNotes entry.
Solution version bumped 3.3.16 -> 3.3.17. Existing rules are unchanged apart
from the version-string bump, and ARM-TTK stays at the package baseline.
@earbona23

Copy link
Copy Markdown
Author

Update — the rule is now packaged into the solution, and I've confirmed the detection against real data.

Packaging (pushed). Instead of committing a full tool regeneration (which rewrote all 73 existing rules with key-order churn and empty groupBy* arrays, taking ARM-TTK from 1 to 2 failures), I added only this rule to the existing package in canonical style:

  • analyticRuleObject74 + its contentTemplates resource + the contentPackages dependency in mainTemplate.json
  • the analytic74 element in createUiDefinition.json
  • registered the rule in Data/Solution_AAD.json, solution version bumped 3.3.163.3.17, regenerated 3.3.17.zip, and added the ReleaseNotes entry

The 73 existing rules are untouched apart from the 3.3.163.3.17 version-string bump, and ARM-TTK stays at the package's existing baseline — the pre-existing IDs Should Be Derived From ResourceIDs result, with no new failures.

Validation against real data. I ran the rule in a production tenant against live AuditLogs Consent to application events, and it fires correctly on the real end-user consent + offline_access + delegated-mailbox-scope pattern. I can't attach a screenshot because those events belong to a customer tenant and are under client confidentiality. As an alternative I'm happy to share the DetectionTemplateSchemaValidation result or walk through the KQL against the AuditLogs schema — and if a screenshot is strictly required I can reproduce one from a non-customer tenant. Let me know which works best.

@v-rusraut

Copy link
Copy Markdown
Contributor

Hi eduardarbona (@earbona23) ,
Please review and fix copilot review comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Analytic Rules Solution Solution specialty review needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants